Compare commits
94 Commits
feat/wicke
...
24126eb834
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24126eb834 | ||
|
|
9a1976b6af | ||
|
|
a8c7e82507 | ||
|
|
889afd0d01 | ||
|
|
82f0fab1f5 | ||
|
|
91e6425c1e | ||
|
|
f54742b041 | ||
|
|
21919c813e | ||
|
|
8ecfc7bf99 | ||
|
|
28ef801c9a | ||
|
|
55b39e93bf | ||
|
|
9bebdef5a5 | ||
|
|
d5c0276fcc | ||
|
|
10c93fff2f | ||
|
|
332827c6da | ||
|
|
46d042b85e | ||
|
|
48ec78197b | ||
|
|
31009560c2 | ||
|
|
16c277d0a9 | ||
|
|
6951efdc85 | ||
|
|
d1fb766fef | ||
|
|
a69faf0582 | ||
|
|
9b0228e7ee | ||
|
|
c5c64a87b8 | ||
|
|
4b671a211b | ||
|
|
e4d778355e | ||
|
|
eefa6c3ce2 | ||
|
|
fb447f175f | ||
|
|
ab436d8c5c | ||
|
|
6b47114015 | ||
|
|
39636039f5 | ||
|
|
5fa31f4311 | ||
|
|
070e8d0f2a | ||
|
|
89bda03f45 | ||
|
|
935974b70a | ||
|
|
f0de3dac8a | ||
|
|
9336d974ba | ||
|
|
0ede98efa4 | ||
|
|
1a7fb818df | ||
|
|
9b5d8fcf97 | ||
|
|
748f839346 | ||
|
|
83e76f71bf | ||
|
|
20ae3de7a3 | ||
|
|
6d2a6ed027 | ||
|
|
60b7bc6aa7 | ||
|
|
8d2127fce2 | ||
|
|
2fb0e1fdbb | ||
|
|
cc03bfad49 | ||
|
|
3250a6f25d | ||
|
|
ca35abe01d | ||
|
|
f2448383d4 | ||
|
|
9be7a01d93 | ||
|
|
569303b631 | ||
|
|
259837622a | ||
|
|
cf34c9c7a6 | ||
|
|
1261efc4c8 | ||
|
|
a101885d87 | ||
|
|
0b6eec0c7b | ||
|
|
8f9795d773 | ||
|
|
d1308bc988 | ||
|
|
758d461c1a | ||
|
|
67d3bcde5b | ||
|
|
bc214867b0 | ||
|
|
4bc40967c8 | ||
|
|
039ae7d1f8 | ||
|
|
f285d689b4 | ||
|
|
5581718ece | ||
|
|
b74fd49816 | ||
|
|
8ad2194d48 | ||
|
|
94d1145668 | ||
|
|
dc228b18ba | ||
|
|
38fc0059ea | ||
|
|
11340e5e58 | ||
|
|
ceb5a13297 | ||
|
|
7694e50fa0 | ||
|
|
e86b84ba61 | ||
|
|
f1750cb887 | ||
|
|
2aae7fe8ea | ||
|
|
b8efa1e26d | ||
|
|
9e6c48dc5c | ||
|
|
6e3413a128 | ||
|
|
0a3a913f66 | ||
|
|
dcaa83e756 | ||
|
|
213a11e731 | ||
|
|
ce0752bbda | ||
|
|
e983b68834 | ||
|
|
d5326c7dc6 | ||
|
|
fdad3bfb7b | ||
|
|
c3f5d72a91 | ||
|
|
241256e373 | ||
|
|
7f36abbcec | ||
|
|
73eefdd7bd | ||
|
|
29f1d75969 | ||
|
|
340f655102 |
24
.env.example
24
.env.example
@@ -1,18 +1,18 @@
|
||||
# Application data
|
||||
RAILS_ENV=development
|
||||
RAILS_ENV=production
|
||||
SECRET_KEY_BASE=a3f5c6e7b8d9e0f1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7
|
||||
DEVISE_SECRET_KEY=your_devise_secret_key_here
|
||||
APP_NAME=Aperonight
|
||||
|
||||
# Database Configuration for production and development
|
||||
DB_HOST=localhost
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
DB_ROOT_PASSWORD=root
|
||||
DB_DATABASE=aperonight
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=root
|
||||
|
||||
# Test database
|
||||
DB_TEST_ADAPTER=sqlite3
|
||||
DB_TEST_DATABASE=aperonight_test
|
||||
DB_TEST_USERNAME=root
|
||||
DB_TEST_USERNAME=root
|
||||
@@ -28,16 +28,18 @@ SMTP_PORT=1025
|
||||
# SMTP_DOMAIN=localhost
|
||||
SMTP_AUTHENTICATION=plain
|
||||
SMTP_ENABLE_STARTTLS=false
|
||||
|
||||
# Production SMTP Configuration (set these in .env.production)
|
||||
# SMTP_ADDRESS=smtp.example.com
|
||||
# SMTP_PORT=587
|
||||
# SMTP_USERNAME=your_smtp_username
|
||||
# SMTP_PASSWORD=your_smtp_password
|
||||
# SMTP_AUTHENTICATION=plain
|
||||
# SMTP_DOMAIN=example.com
|
||||
# SMTP_STARTTLS=true
|
||||
|
||||
# Invoice Emitter Configuration
|
||||
INVOICE_COMPANY_NAME=AperoNight
|
||||
INVOICE_COMPANY_ADDRESS_LINE_1=123 Avenue des Événements
|
||||
INVOICE_COMPANY_ADDRESS_LINE_2=75000 Paris, France
|
||||
INVOICE_COMPANY_EMAIL=contact@apero-night.fr
|
||||
INVOICE_COMPANY_PHONE=
|
||||
INVOICE_COMPANY_WEBSITE=
|
||||
INVOICE_COMPANY_VAT_NUMBER=
|
||||
INVOICE_COMPANY_SIRET=
|
||||
|
||||
# Application variables
|
||||
STRIPE_PUBLISHABLE_KEY=pk_test_51S1M7BJWx6G2LLIXYpTvi0hxMpZ4tZSxkmr2Wbp1dQ73MKNp4Tyu4xFJBqLXK5nn4E0nEf2tdgJqEwWZLosO3QGn00kMvjXWGW
|
||||
STRIPE_SECRET_KEY=sk_test_51S1M7BJWx6G2LLIXK2pdLpRKb9Mgd3sZ30N4ueVjHepgxQKbWgMVJoa4v4ESzHQ6u6zJjO4jUvgLYPU1QLyAiFTN00sGz2ortW
|
||||
|
||||
93
.gitea/workflows/ai-code-review.yaml.old
Normal file
93
.gitea/workflows/ai-code-review.yaml.old
Normal file
@@ -0,0 +1,93 @@
|
||||
name: AI Code Review
|
||||
run-name: AI Code Review by @${{ github.actor }} 🤖
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
ai-review:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get PR diff
|
||||
id: diff
|
||||
run: |
|
||||
# Get the diff for the PR
|
||||
git fetch origin ${{ github.base_ref }}
|
||||
DIFF=$(git diff origin/${{ github.base_ref }}...HEAD)
|
||||
echo "diff<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$DIFF" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: AI Code Review
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
# Or use ANTHROPIC_API_KEY for Claude
|
||||
# ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
run: |
|
||||
# Install dependencies
|
||||
pip install openai requests
|
||||
|
||||
# Create review script
|
||||
cat > review.py << 'EOF'
|
||||
import os
|
||||
import openai
|
||||
import requests
|
||||
import json
|
||||
|
||||
# Configure OpenAI client (or use Anthropic client for Claude)
|
||||
client = openai.OpenAI(api_key=os.environ['OPENAI_API_KEY'])
|
||||
|
||||
# Get diff from environment
|
||||
diff = """${{ steps.diff.outputs.diff }}"""
|
||||
|
||||
if not diff.strip():
|
||||
print("No changes to review")
|
||||
exit(0)
|
||||
|
||||
# Create review prompt
|
||||
prompt = f"""
|
||||
Please review this code diff and provide constructive feedback:
|
||||
|
||||
{diff}
|
||||
|
||||
Focus on:
|
||||
- Code quality and best practices
|
||||
- Potential bugs or security issues
|
||||
- Performance considerations
|
||||
- Maintainability and readability
|
||||
- Ruby on Rails specific patterns
|
||||
|
||||
Provide your review as structured feedback with specific line references where possible.
|
||||
"""
|
||||
|
||||
try:
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4", # or "claude-3-sonnet" for Claude
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
max_tokens=2000
|
||||
)
|
||||
|
||||
review = response.choices[0].message.content
|
||||
print("AI Code Review:")
|
||||
print("=" * 50)
|
||||
print(review)
|
||||
|
||||
# Post review as PR comment (requires additional API setup)
|
||||
# This would need Gitea API integration
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during review: {e}")
|
||||
EOF
|
||||
|
||||
python review.py
|
||||
|
||||
- name: Comment on PR
|
||||
if: always()
|
||||
run: |
|
||||
echo "Review completed - implement Gitea API integration to post comments"
|
||||
98
.gitea/workflows/ci-mariadb.yaml
Normal file
98
.gitea/workflows/ci-mariadb.yaml
Normal file
@@ -0,0 +1,98 @@
|
||||
name: Ruby on Rails Test
|
||||
run-name: Deploy to ${{ inputs.deploy_target }} by @${{ github.actor }} 🚀
|
||||
#on: [push]
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
rails-test:
|
||||
runs-on: ubuntu-22.04
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb:11.7.2-noble
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD:-root}"
|
||||
MYSQL_DATABASE: "${DB_DATABASE:-aperonight_test}"
|
||||
MYSQL_USER: "${DB_USERNAME:-aperonight}"
|
||||
MYSQL_PASSWORD: "${DB_PASSWORD:-aperonight}"
|
||||
# RUNNER_TOOL_CACHE: /toolcache
|
||||
#ports:
|
||||
# - "3306:3306"
|
||||
#options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
options: >-
|
||||
--health-cmd="healthcheck.sh --connect --innodb_initialized"
|
||||
--health-interval=10s
|
||||
--health-timeout=5s
|
||||
--health-retries=3
|
||||
env:
|
||||
RAILS_ENV: test
|
||||
DB_HOST: mariadb
|
||||
DB_ROOT_PASSWORD: "${DB_ROOT_PASSWORD:-root}"
|
||||
DB_DATABASE: "${DB_DATABASE:-aperonight_test}"
|
||||
DB_USERNAME: "${DB_USERNAME:-root}"
|
||||
DB_PASSWORD: "${DB_PASSWORD:-root}"
|
||||
RUNNER_TOOL_CACHE: /toolcache # https://about.gitea.com/resources/tutorials/enable-gitea-actions-cache-to-accelerate-cicd
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: .ruby-version # Not needed with a .ruby-version, .tool-versions or mise.toml
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
echo "📦 Installing dependencies..."
|
||||
gem install bundler
|
||||
bundle install --jobs 4 --retry 3
|
||||
npm install -g yarn
|
||||
yarn install
|
||||
echo "📦 Dependencies installed!"
|
||||
|
||||
- name: Cache bundle
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
/usr/local/bundle
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/Gemfile.lock') }}
|
||||
restore-keys: |-
|
||||
${{ runner.os }}-${{ hashFiles('**/Gemfile.lock') }}
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |-
|
||||
${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- name: Run migrations
|
||||
run: |
|
||||
echo "🔄 Running migrations..."
|
||||
bundle exec rails db:drop
|
||||
bundle exec rails db:setup
|
||||
bundle exec rails db:migrate
|
||||
echo "🔄 Migrations complete!"
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
echo "🧪 Running tests..."
|
||||
bundle exec rails test
|
||||
echo "🧪 Tests complete!"
|
||||
|
||||
- name: Run linter
|
||||
run: |
|
||||
echo "🚫 Running linter..."
|
||||
bundle exec rubocop
|
||||
echo "🚫 Linter complete!"
|
||||
82
.gitea/workflows/ci-sqlite.yaml.old
Normal file
82
.gitea/workflows/ci-sqlite.yaml.old
Normal file
@@ -0,0 +1,82 @@
|
||||
name: Ruby on Rails Test
|
||||
run-name: Deploy to ${{ inputs.deploy_target }} by @${{ github.actor }} 🚀
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
rails-test:
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
RAILS_ENV: test
|
||||
# SQLite does not require these variables, but you can keep them for consistency
|
||||
DB_TEST_ADAPTER: "sqlite3"
|
||||
DB_TEST_DATABASE: "data/test.sqlite" # Default SQLite database file path
|
||||
DB_TEST_USERNAME: "root"
|
||||
DB_TEST_PASSWORD: "root"
|
||||
RUNNER_TOOL_CACHE: /toolcache # Optional, for caching
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: .ruby-version # Not needed with a .ruby-version, .tool-versions or mise.toml
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
echo "📦 Installing dependencies..."
|
||||
gem install bundler
|
||||
bundle install --jobs 4 --retry 3
|
||||
npm install -g yarn
|
||||
yarn install
|
||||
echo "📦 Dependencies installed!"
|
||||
|
||||
- name: Cache bundle
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
/usr/local/bundle
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/Gemfile.lock') }}
|
||||
restore-keys: |-
|
||||
${{ runner.os }}-${{ hashFiles('**/Gemfile.lock') }}
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |-
|
||||
${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- name: Run migrations
|
||||
run: |
|
||||
echo "🔄 Running migrations..."
|
||||
bundle exec rails db:drop
|
||||
bundle exec rails db:setup
|
||||
bundle exec rails db:migrate
|
||||
echo "🔄 Migrations complete!"
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
echo "🧪 Running tests..."
|
||||
bundle exec rails test
|
||||
echo "🧪 Tests complete!"
|
||||
|
||||
- name: Run linter
|
||||
run: |
|
||||
echo "🚫 Running linter..."
|
||||
bundle exec rubocop
|
||||
echo "🚫 Linter complete!"
|
||||
45
.gitea/workflows/linter.yaml
Normal file
45
.gitea/workflows/linter.yaml
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Ruby on Rails Test
|
||||
run-name: Deploy to ${{ inputs.deploy_target }} by @${{ github.actor }} 🚀
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
rails-test:
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
RAILS_ENV: test
|
||||
# SQLite does not require these variables, but you can keep them for consistency
|
||||
DB_TEST_ADAPTER: "sqlite3"
|
||||
DB_TEST_DATABASE: "data/test.sqlite" # Default SQLite database file path
|
||||
DB_TEST_USERNAME: "root"
|
||||
DB_TEST_PASSWORD: "root"
|
||||
RUNNER_TOOL_CACHE: /toolcache # Optional, for caching
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: .ruby-version # Not needed with a .ruby-version, .tool-versions or mise.toml
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
echo "📦 Installing dependencies..."
|
||||
gem install bundler
|
||||
bundle install --jobs 4 --retry 3
|
||||
npm install -g yarn
|
||||
yarn install
|
||||
echo "📦 Dependencies installed!"
|
||||
|
||||
- name: Run linter
|
||||
run: |
|
||||
echo "🚫 Running linter..."
|
||||
bundle exec rubocop
|
||||
echo "🚫 Linter complete!"
|
||||
804
.superdesign/design_iterations/aperonight_design_system.css
Normal file
804
.superdesign/design_iterations/aperonight_design_system.css
Normal file
@@ -0,0 +1,804 @@
|
||||
/**
|
||||
* Aperonight Design System
|
||||
* Generated from homepage analysis
|
||||
* A modern, professional design system for event platforms
|
||||
*/
|
||||
|
||||
/* === ROOT VARIABLES === */
|
||||
:root {
|
||||
/* Brand Colors */
|
||||
--brand-primary: #667eea;
|
||||
--brand-secondary: #764ba2;
|
||||
--brand-accent: #facc15; /* yellow-400 */
|
||||
--brand-accent-dark: #eab308; /* yellow-500 */
|
||||
|
||||
/* Neutral Colors */
|
||||
--color-white: #ffffff;
|
||||
--color-black: #000000;
|
||||
--color-gray-50: #f9fafb;
|
||||
--color-gray-100: #f3f4f6;
|
||||
--color-gray-200: #e5e7eb;
|
||||
--color-gray-300: #d1d5db;
|
||||
--color-gray-400: #9ca3af;
|
||||
--color-gray-500: #6b7280;
|
||||
--color-gray-600: #4b5563;
|
||||
--color-gray-700: #374151;
|
||||
--color-gray-800: #1f2937;
|
||||
--color-gray-900: #111827;
|
||||
|
||||
/* Purple Shades */
|
||||
--color-purple-600: #9333ea;
|
||||
--color-purple-700: #7c3aed;
|
||||
--color-purple-800: #6b21a8;
|
||||
|
||||
/* Blue Shades */
|
||||
--color-blue-600: #2563eb;
|
||||
--color-blue-700: #1d4ed8;
|
||||
|
||||
/* Typography */
|
||||
--font-family-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
||||
--font-family-mono: ui-monospace, SFMono-Regular, 'SF Mono', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
|
||||
/* Font Sizes */
|
||||
--text-xs: 0.75rem; /* 12px */
|
||||
--text-sm: 0.875rem; /* 14px */
|
||||
--text-base: 1rem; /* 16px */
|
||||
--text-lg: 1.125rem; /* 18px */
|
||||
--text-xl: 1.25rem; /* 20px */
|
||||
--text-2xl: 1.5rem; /* 24px */
|
||||
--text-3xl: 1.875rem; /* 30px */
|
||||
--text-4xl: 2.25rem; /* 36px */
|
||||
--text-5xl: 3rem; /* 48px */
|
||||
--text-6xl: 3.75rem; /* 60px */
|
||||
|
||||
/* Font Weights */
|
||||
--font-medium: 500;
|
||||
--font-semibold: 600;
|
||||
--font-bold: 700;
|
||||
|
||||
/* Spacing Scale */
|
||||
--space-1: 0.25rem; /* 4px */
|
||||
--space-2: 0.5rem; /* 8px */
|
||||
--space-3: 0.75rem; /* 12px */
|
||||
--space-4: 1rem; /* 16px */
|
||||
--space-6: 1.5rem; /* 24px */
|
||||
--space-8: 2rem; /* 32px */
|
||||
--space-12: 3rem; /* 48px */
|
||||
--space-16: 4rem; /* 64px */
|
||||
--space-24: 6rem; /* 96px */
|
||||
|
||||
/* Border Radius */
|
||||
--radius-sm: 0.375rem; /* 6px */
|
||||
--radius-md: 0.5rem; /* 8px */
|
||||
--radius-lg: 0.75rem; /* 12px */
|
||||
--radius-xl: 1rem; /* 16px */
|
||||
--radius-2xl: 1.25rem; /* 20px */
|
||||
--radius-3xl: 1.5rem; /* 24px */
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||
|
||||
/* Gradients */
|
||||
--gradient-primary: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-secondary) 100%);
|
||||
--gradient-overlay: rgba(0, 0, 0, 0.3);
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: all 0.2s ease;
|
||||
--transition-medium: all 0.3s ease;
|
||||
--transition-slow: all 0.5s ease;
|
||||
}
|
||||
|
||||
/* === BASE STYLES === */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
line-height: 1.5;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-family: var(--font-family-sans);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family-sans);
|
||||
line-height: 1.6;
|
||||
color: var(--color-gray-900);
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
|
||||
/* === TYPOGRAPHY SYSTEM === */
|
||||
.text-xs { font-size: var(--text-xs); }
|
||||
.text-sm { font-size: var(--text-sm); }
|
||||
.text-base { font-size: var(--text-base); }
|
||||
.text-lg { font-size: var(--text-lg); }
|
||||
.text-xl { font-size: var(--text-xl); }
|
||||
.text-2xl { font-size: var(--text-2xl); }
|
||||
.text-3xl { font-size: var(--text-3xl); }
|
||||
.text-4xl { font-size: var(--text-4xl); }
|
||||
.text-5xl { font-size: var(--text-5xl); }
|
||||
.text-6xl { font-size: var(--text-6xl); }
|
||||
|
||||
.font-medium { font-weight: var(--font-medium); }
|
||||
.font-semibold { font-weight: var(--font-semibold); }
|
||||
.font-bold { font-weight: var(--font-bold); }
|
||||
|
||||
.leading-tight { line-height: 1.25; }
|
||||
.leading-normal { line-height: 1.5; }
|
||||
.leading-relaxed { line-height: 1.625; }
|
||||
|
||||
/* === BUTTON SYSTEM === */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-3) var(--space-6);
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-semibold);
|
||||
border-radius: var(--radius-full);
|
||||
transition: var(--transition-fast);
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--color-white);
|
||||
color: var(--color-gray-900);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--color-gray-100);
|
||||
box-shadow: var(--shadow-xl);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: transparent;
|
||||
color: var(--color-white);
|
||||
border: 2px solid var(--color-white);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: var(--color-white);
|
||||
color: var(--color-gray-900);
|
||||
}
|
||||
|
||||
.btn-accent {
|
||||
background-color: var(--color-purple-600);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.btn-accent:hover {
|
||||
background-color: var(--color-purple-700);
|
||||
}
|
||||
|
||||
.btn-dark {
|
||||
background-color: var(--color-gray-900);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.btn-dark:hover {
|
||||
background-color: var(--color-gray-800);
|
||||
}
|
||||
|
||||
/* Button Sizes */
|
||||
.btn-sm {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: var(--space-4) var(--space-8);
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
/* === CARD SYSTEM === */
|
||||
.card {
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-2xl);
|
||||
box-shadow: var(--shadow-sm);
|
||||
overflow: hidden;
|
||||
transition: var(--transition-medium);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-event {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-event-image {
|
||||
aspect-ratio: 4/3;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-2xl);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-event-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: var(--transition-medium);
|
||||
}
|
||||
|
||||
.card-event:hover .card-event-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.card-event-badge {
|
||||
position: absolute;
|
||||
top: var(--space-4);
|
||||
left: var(--space-4);
|
||||
background-color: var(--brand-accent);
|
||||
color: var(--color-gray-900);
|
||||
padding: var(--space-1) var(--space-3);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.card-event-price {
|
||||
position: absolute;
|
||||
bottom: var(--space-4);
|
||||
right: var(--space-4);
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(4px);
|
||||
color: var(--color-gray-900);
|
||||
padding: var(--space-1) var(--space-3);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-bold);
|
||||
}
|
||||
|
||||
.card-event-content {
|
||||
padding: var(--space-6);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-event-title {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--color-gray-900);
|
||||
margin-bottom: var(--space-2);
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
|
||||
.card-event:hover .card-event-title {
|
||||
color: var(--color-purple-600);
|
||||
}
|
||||
|
||||
.card-event-meta {
|
||||
color: var(--color-gray-600);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.card-event-description {
|
||||
color: var(--color-gray-500);
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--leading-relaxed);
|
||||
max-width: 20rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* === HERO SYSTEM === */
|
||||
.hero {
|
||||
background: var(--gradient-primary);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--gradient-overlay);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: var(--text-4xl);
|
||||
font-weight: var(--font-bold);
|
||||
line-height: var(--leading-tight);
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: var(--text-xl);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: var(--space-8);
|
||||
max-width: 32rem;
|
||||
}
|
||||
|
||||
.hero-accent {
|
||||
color: var(--brand-accent);
|
||||
}
|
||||
|
||||
/* Responsive Hero */
|
||||
@media (min-width: 1024px) {
|
||||
.hero-title {
|
||||
font-size: var(--text-6xl);
|
||||
}
|
||||
}
|
||||
|
||||
/* === METRICS SYSTEM === */
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--space-8);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.metrics-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
transition: var(--transition-medium);
|
||||
}
|
||||
|
||||
.metric-number {
|
||||
font-size: var(--text-4xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--color-purple-600);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.metric-number {
|
||||
font-size: var(--text-5xl);
|
||||
}
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: var(--color-gray-600);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
/* === SECTION SYSTEM === */
|
||||
.section {
|
||||
padding: var(--space-16) 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--space-12);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--color-gray-900);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.section-title {
|
||||
font-size: var(--text-4xl);
|
||||
}
|
||||
}
|
||||
|
||||
.section-description {
|
||||
font-size: var(--text-xl);
|
||||
color: var(--color-gray-600);
|
||||
max-width: 40rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* === GRID SYSTEM === */
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: var(--space-8);
|
||||
}
|
||||
|
||||
.grid-1 { grid-template-columns: 1fr; }
|
||||
.grid-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.grid-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.grid-md-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.grid-md-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.grid-lg-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.grid-lg-4 { grid-template-columns: repeat(4, 1fr); }
|
||||
}
|
||||
|
||||
/* === UTILITY CLASSES === */
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding-left: var(--space-4);
|
||||
padding-right: var(--space-4);
|
||||
}
|
||||
|
||||
.text-center { text-align: center; }
|
||||
.text-left { text-align: left; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
.bg-white { background-color: var(--color-white); }
|
||||
.bg-gray-50 { background-color: var(--color-gray-50); }
|
||||
.bg-gray-900 { background-color: var(--color-gray-900); }
|
||||
|
||||
.text-white { color: var(--color-white); }
|
||||
.text-gray-600 { color: var(--color-gray-600); }
|
||||
.text-gray-900 { color: var(--color-gray-900); }
|
||||
|
||||
.rounded-full { border-radius: var(--radius-full); }
|
||||
.rounded-2xl { border-radius: var(--radius-2xl); }
|
||||
|
||||
.shadow-lg { box-shadow: var(--shadow-lg); }
|
||||
.shadow-xl { box-shadow: var(--shadow-xl); }
|
||||
|
||||
.mb-2 { margin-bottom: var(--space-2); }
|
||||
.mb-4 { margin-bottom: var(--space-4); }
|
||||
.mb-6 { margin-bottom: var(--space-6); }
|
||||
.mb-8 { margin-bottom: var(--space-8); }
|
||||
.mb-12 { margin-bottom: var(--space-12); }
|
||||
|
||||
.p-4 { padding: var(--space-4); }
|
||||
.p-6 { padding: var(--space-6); }
|
||||
.p-8 { padding: var(--space-8); }
|
||||
|
||||
.flex { display: flex; }
|
||||
.items-center { align-items: center; }
|
||||
.justify-center { justify-content: center; }
|
||||
.gap-4 { gap: var(--space-4); }
|
||||
|
||||
.transition { transition: var(--transition-fast); }
|
||||
|
||||
.max-w-lg { max-width: 32rem; }
|
||||
.max-w-2xl { max-width: 42rem; }
|
||||
.max-w-4xl { max-width: 56rem; }
|
||||
|
||||
/* === BREADCRUMB SYSTEM === */
|
||||
.breadcrumb {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
background-color: var(--color-white);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.breadcrumb-item a {
|
||||
color: var(--color-gray-700);
|
||||
text-decoration: none;
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
|
||||
.breadcrumb-item a:hover {
|
||||
color: var(--color-purple-600);
|
||||
}
|
||||
|
||||
.breadcrumb-item:not(:last-child)::after {
|
||||
content: '';
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-left: var(--space-2);
|
||||
background: url("data:image/svg+xml,%3csvg fill='%234b5563' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill-rule='evenodd' d='M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z' clip-rule='evenodd'/%3e%3c/svg%3e") center no-repeat;
|
||||
background-size: 1rem;
|
||||
}
|
||||
|
||||
.breadcrumb-current {
|
||||
color: var(--color-purple-600);
|
||||
}
|
||||
|
||||
/* === PAGE HEADER SYSTEM === */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: var(--space-8) 0;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--color-gray-900);
|
||||
}
|
||||
|
||||
.page-meta {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
/* === EVENTS GRID SYSTEM === */
|
||||
.events-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-6);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.events-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.events-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.event-card {
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow-md);
|
||||
overflow: hidden;
|
||||
transition: var(--transition-medium);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.event-card:hover {
|
||||
box-shadow: var(--shadow-xl);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.event-card-image {
|
||||
height: 12rem;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.event-card-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: var(--transition-medium);
|
||||
}
|
||||
|
||||
.event-card:hover .event-card-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.event-card-placeholder {
|
||||
height: 12rem;
|
||||
background: var(--gradient-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.event-card-placeholder svg {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.event-card-content {
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.event-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.event-card-title {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--color-gray-900);
|
||||
margin-bottom: var(--space-1);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.event-card-venue {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-gray-500);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.event-card-date {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: var(--space-2) calc(var(--space-2) + var(--space-1));
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--font-medium);
|
||||
background-color: rgba(147, 51, 234, 0.1);
|
||||
color: var(--color-purple-800);
|
||||
white-space: nowrap;
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.event-card-description {
|
||||
color: var(--color-gray-600);
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1.4;
|
||||
margin-bottom: var(--space-4);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.event-card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.event-card-price {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--color-gray-900);
|
||||
}
|
||||
|
||||
.event-card-price-unavailable {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
.event-card-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border: 1px solid transparent;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
color: var(--color-white);
|
||||
background: var(--gradient-primary);
|
||||
text-decoration: none;
|
||||
transition: var(--transition-fast);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.event-card-link:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* === EMPTY STATE SYSTEM === */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: var(--space-16) var(--space-4);
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
margin: 0 auto var(--space-6);
|
||||
background: linear-gradient(135deg, rgba(147, 51, 234, 0.1) 0%, rgba(79, 70, 229, 0.1) 100%);
|
||||
border-radius: var(--radius-full);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.empty-state-icon svg {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
color: var(--color-purple-600);
|
||||
}
|
||||
|
||||
.empty-state-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--color-gray-900);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.empty-state-description {
|
||||
color: var(--color-gray-500);
|
||||
margin-bottom: var(--space-6);
|
||||
max-width: 24rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* === PAGINATION SYSTEM === */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: var(--space-8);
|
||||
}
|
||||
|
||||
.pagination .page-item {
|
||||
margin: 0 var(--space-1);
|
||||
}
|
||||
|
||||
.pagination .page-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--color-gray-600);
|
||||
background-color: var(--color-white);
|
||||
border: 1px solid var(--color-gray-200);
|
||||
border-radius: var(--radius-md);
|
||||
text-decoration: none;
|
||||
transition: var(--transition-fast);
|
||||
min-width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.pagination .page-link:hover {
|
||||
background-color: var(--color-gray-50);
|
||||
border-color: var(--color-purple-300);
|
||||
color: var(--color-purple-600);
|
||||
}
|
||||
|
||||
.pagination .page-item.active .page-link {
|
||||
background-color: var(--color-purple-600);
|
||||
border-color: var(--color-purple-600);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.pagination .page-item.disabled .page-link {
|
||||
color: var(--color-gray-300);
|
||||
background-color: var(--color-white);
|
||||
border-color: var(--color-gray-200);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* === RESPONSIVE UTILITIES === */
|
||||
@media (max-width: 640px) {
|
||||
.sm\:flex-col { flex-direction: column; }
|
||||
.sm\:text-center { text-align: center; }
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:flex-row { flex-direction: row; }
|
||||
.sm\:flex-1 { flex: 1; }
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.lg\:justify-start { justify-content: flex-start; }
|
||||
.lg\:text-left { text-align: left; }
|
||||
}
|
||||
483
.superdesign/design_iterations/aperonight_design_system_1.html
Normal file
483
.superdesign/design_iterations/aperonight_design_system_1.html
Normal file
@@ -0,0 +1,483 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Aperonight Design System</title>
|
||||
<link rel="stylesheet" href="aperonight_design_system.css">
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<style>
|
||||
/* Additional showcase styles */
|
||||
.showcase-section {
|
||||
padding: 3rem 0;
|
||||
border-bottom: 1px solid var(--color-gray-200);
|
||||
}
|
||||
|
||||
.showcase-grid {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--color-gray-200);
|
||||
}
|
||||
|
||||
.color-circle {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--color-gray-300);
|
||||
}
|
||||
|
||||
.component-demo {
|
||||
padding: 2rem;
|
||||
background: var(--color-gray-50);
|
||||
border-radius: var(--radius-xl);
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: var(--color-white);
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid var(--color-gray-200);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: var(--color-gray-600);
|
||||
text-decoration: none;
|
||||
font-weight: var(--font-medium);
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--color-purple-600);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--color-gray-900);
|
||||
}
|
||||
|
||||
/* Fixed secondary button for light backgrounds */
|
||||
.btn-secondary-alt {
|
||||
background-color: transparent;
|
||||
color: var(--color-gray-700);
|
||||
border: 2px solid var(--color-gray-300);
|
||||
}
|
||||
|
||||
.btn-secondary-alt:hover {
|
||||
background-color: var(--color-gray-100);
|
||||
color: var(--color-gray-900);
|
||||
border-color: var(--color-gray-400);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar">
|
||||
<div class="container navbar-content">
|
||||
<div class="logo">Aperonight Design System</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="#colors" class="nav-link">Couleurs</a></li>
|
||||
<li><a href="#typography" class="nav-link">Typographie</a></li>
|
||||
<li><a href="#buttons" class="nav-link">Boutons</a></li>
|
||||
<li><a href="#cards" class="nav-link">Cartes</a></li>
|
||||
<li><a href="#components" class="nav-link">Composants</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="hero">
|
||||
<div class="hero-content">
|
||||
<div class="container">
|
||||
<div class="text-center">
|
||||
<h1 class="hero-title">
|
||||
Système de Design
|
||||
<span class="hero-accent">Aperonight</span>
|
||||
</h1>
|
||||
<p class="hero-subtitle">
|
||||
Un système de design moderne et cohérent pour créer des expériences exceptionnelles dans le domaine des événements après-travail.
|
||||
</p>
|
||||
<div class="flex gap-4 justify-center">
|
||||
<a href="#colors" class="btn btn-primary btn-lg">
|
||||
<i data-lucide="palette" class="w-5 h-5"></i>
|
||||
Explorer les Composants
|
||||
</a>
|
||||
<a href="#" class="btn btn-secondary btn-lg">
|
||||
<i data-lucide="download" class="w-5 h-5"></i>
|
||||
Télécharger
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Color Palette -->
|
||||
<section id="colors" class="showcase-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Palette de Couleurs</h2>
|
||||
<p class="section-description">
|
||||
Les couleurs de base du système Aperonight, conçues pour transmettre professionnalisme et modernité.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="showcase-grid">
|
||||
<div>
|
||||
<h3 class="text-2xl font-semibold mb-4">Couleurs de Marque</h3>
|
||||
<div class="grid grid-1 gap-4">
|
||||
<div class="color-swatch">
|
||||
<div class="color-circle" style="background: #667eea;"></div>
|
||||
<div>
|
||||
<div class="font-semibold">Primary Blue</div>
|
||||
<div class="text-sm text-gray-600">#667eea</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="color-swatch">
|
||||
<div class="color-circle" style="background: #764ba2;"></div>
|
||||
<div>
|
||||
<div class="font-semibold">Secondary Purple</div>
|
||||
<div class="text-sm text-gray-600">#764ba2</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="color-swatch">
|
||||
<div class="color-circle" style="background: #facc15;"></div>
|
||||
<div>
|
||||
<div class="font-semibold">Accent Yellow</div>
|
||||
<div class="text-sm text-gray-600">#facc15</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-2xl font-semibold mb-4">Couleurs Neutres</h3>
|
||||
<div class="grid grid-1 gap-4">
|
||||
<div class="color-swatch">
|
||||
<div class="color-circle" style="background: #ffffff; border: 2px solid #e5e7eb;"></div>
|
||||
<div>
|
||||
<div class="font-semibold">White</div>
|
||||
<div class="text-sm text-gray-600">#ffffff</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="color-swatch">
|
||||
<div class="color-circle" style="background: #f3f4f6;"></div>
|
||||
<div>
|
||||
<div class="font-semibold">Gray 100</div>
|
||||
<div class="text-sm text-gray-600">#f3f4f6</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="color-swatch">
|
||||
<div class="color-circle" style="background: #4b5563;"></div>
|
||||
<div>
|
||||
<div class="font-semibold">Gray 600</div>
|
||||
<div class="text-sm text-gray-600">#4b5563</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="color-swatch">
|
||||
<div class="color-circle" style="background: #111827;"></div>
|
||||
<div>
|
||||
<div class="font-semibold">Gray 900</div>
|
||||
<div class="text-sm text-gray-600">#111827</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Typography -->
|
||||
<section id="typography" class="showcase-section bg-gray-50">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Typographie</h2>
|
||||
<p class="section-description">
|
||||
Une hiérarchie typographique claire et lisible pour tous les contenus.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="component-demo bg-white">
|
||||
<h1 class="text-6xl font-bold mb-4">Hero Title - 60px Bold</h1>
|
||||
<h2 class="text-4xl font-bold mb-4">Section Title - 36px Bold</h2>
|
||||
<h3 class="text-2xl font-semibold mb-4">Card Title - 24px Semibold</h3>
|
||||
<p class="text-xl mb-4">Large Text - 20px Regular</p>
|
||||
<p class="text-base mb-4">Body Text - 16px Regular</p>
|
||||
<p class="text-sm text-gray-600">Small Text - 14px Regular</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Buttons -->
|
||||
<section id="buttons" class="showcase-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Système de Boutons</h2>
|
||||
<p class="section-description">
|
||||
Différents styles de boutons pour diverses actions et hiérarchies.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="component-demo">
|
||||
<div class="grid grid-md-2 gap-8">
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold mb-4">Styles Principaux</h3>
|
||||
<div class="flex flex-col gap-4">
|
||||
<button class="btn btn-primary">
|
||||
<i data-lucide="calendar"></i>
|
||||
Bouton Principal
|
||||
</button>
|
||||
<button class="btn btn-secondary-alt">
|
||||
<i data-lucide="user-plus"></i>
|
||||
Bouton Secondaire
|
||||
</button>
|
||||
<button class="btn btn-accent">
|
||||
<i data-lucide="star"></i>
|
||||
Bouton Accent
|
||||
</button>
|
||||
<button class="btn btn-dark">
|
||||
<i data-lucide="arrow-right"></i>
|
||||
Bouton Sombre
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold mb-4">Tailles</h3>
|
||||
<div class="flex flex-col gap-4">
|
||||
<button class="btn btn-primary btn-sm">
|
||||
<i data-lucide="eye"></i>
|
||||
Petit Bouton
|
||||
</button>
|
||||
<button class="btn btn-primary">
|
||||
<i data-lucide="calendar"></i>
|
||||
Bouton Normal
|
||||
</button>
|
||||
<button class="btn btn-primary btn-lg">
|
||||
<i data-lucide="search"></i>
|
||||
Grand Bouton
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Cards -->
|
||||
<section id="cards" class="showcase-section bg-gray-50">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Système de Cartes</h2>
|
||||
<p class="section-description">
|
||||
Cartes événements et composants modulaires.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-md-2 lg:grid-lg-3 gap-8">
|
||||
<!-- Event Card Example -->
|
||||
<div class="card card-event">
|
||||
<div class="card-event-image">
|
||||
<img src="https://images.unsplash.com/photo-1511578314322-379afb476865?w=600&h=400&fit=crop" alt="Événement exemple">
|
||||
<div class="card-event-badge">★ En vedette</div>
|
||||
<div class="card-event-price">À partir de €25</div>
|
||||
</div>
|
||||
<div class="card-event-content">
|
||||
<h3 class="card-event-title">AFTERWORK ROOFTOP</h3>
|
||||
<div class="card-event-meta mb-4">
|
||||
<div class="flex items-center justify-center gap-2 text-sm mb-2">
|
||||
<i data-lucide="calendar" class="w-4 h-4"></i>
|
||||
Vendredi 15 Décembre • 18:30
|
||||
</div>
|
||||
<div class="flex items-center justify-center gap-2 text-sm">
|
||||
<i data-lucide="map-pin" class="w-4 h-4"></i>
|
||||
Rooftop Bar Paris
|
||||
</div>
|
||||
</div>
|
||||
<p class="card-event-description">
|
||||
Rejoignez-nous pour un afterwork exclusif avec vue panoramique sur Paris.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Simple Card -->
|
||||
<div class="card p-6">
|
||||
<h3 class="text-xl font-semibold mb-2">Carte Simple</h3>
|
||||
<p class="text-gray-600">
|
||||
Une carte basique pour du contenu général avec hover effects.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Metric Card -->
|
||||
<div class="card p-6 text-center">
|
||||
<div class="metric-number">2.5k+</div>
|
||||
<div class="metric-label">Membres Actifs</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Components -->
|
||||
<section id="components" class="showcase-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Composants UI</h2>
|
||||
<p class="section-description">
|
||||
Éléments d'interface réutilisables pour construire des expériences cohérentes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="showcase-grid">
|
||||
<!-- Hero Component -->
|
||||
<div class="component-demo">
|
||||
<h3 class="text-xl font-semibold mb-4">Section Hero</h3>
|
||||
<div class="hero" style="min-height: 300px; border-radius: var(--radius-2xl);">
|
||||
<div class="hero-content">
|
||||
<div class="container text-center">
|
||||
<h2 class="hero-title" style="font-size: var(--text-3xl);">
|
||||
Titre <span class="hero-accent">Héro</span>
|
||||
</h2>
|
||||
<p class="hero-subtitle" style="font-size: var(--text-base); max-width: 24rem; margin: 0 auto var(--space-6);">
|
||||
Description du héro avec gradient de fond
|
||||
</p>
|
||||
<button class="btn btn-primary">Action Principale</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metrics Grid -->
|
||||
<div class="component-demo">
|
||||
<h3 class="text-xl font-semibold mb-4">Grille de Métriques</h3>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-item">
|
||||
<div class="metric-number">50+</div>
|
||||
<div class="metric-label">Événements</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-number">2.5k</div>
|
||||
<div class="metric-label">Membres</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-number">12</div>
|
||||
<div class="metric-label">Ce mois-ci</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-number">98%</div>
|
||||
<div class="metric-label">Satisfaction</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Guidelines -->
|
||||
<section class="showcase-section bg-gray-50">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Guide d'Utilisation</h2>
|
||||
<p class="section-description">
|
||||
Principes et bonnes pratiques pour utiliser ce système de design.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-md-2 gap-8">
|
||||
<div class="card p-6">
|
||||
<h3 class="text-xl font-semibold mb-4">✨ Principes de Design</h3>
|
||||
<ul class="space-y-3 text-gray-600">
|
||||
<li><strong>Cohérence</strong> - Utilisez les composants de manière uniforme</li>
|
||||
<li><strong>Accessibilité</strong> - Respectez les contrastes et la lisibilité</li>
|
||||
<li><strong>Responsive</strong> - Adaptez à tous les écrans</li>
|
||||
<li><strong>Performance</strong> - Optimisez les animations et interactions</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card p-6">
|
||||
<h3 class="text-xl font-semibold mb-4">🎨 Utilisation des Couleurs</h3>
|
||||
<ul class="space-y-3 text-gray-600">
|
||||
<li><strong>Primary</strong> - Actions principales et navigation</li>
|
||||
<li><strong>Accent</strong> - Éléments mis en évidence (badges, etc.)</li>
|
||||
<li><strong>Gray</strong> - Textes, bordures et arrière-plans</li>
|
||||
<li><strong>Purple</strong> - Métriques et éléments spéciaux</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-gray-900 text-white py-16">
|
||||
<div class="container text-center">
|
||||
<h3 class="text-2xl font-bold mb-4">Système de Design Aperonight</h3>
|
||||
<p class="text-gray-400 mb-8 max-w-2xl mx-auto">
|
||||
Créé pour maintenir une expérience utilisateur cohérente et professionnelle à travers tous les points de contact Aperonight.
|
||||
</p>
|
||||
<div class="flex gap-4 justify-center">
|
||||
<button class="btn btn-primary">
|
||||
<i data-lucide="download"></i>
|
||||
Télécharger le CSS
|
||||
</button>
|
||||
<button class="btn btn-secondary">
|
||||
<i data-lucide="github"></i>
|
||||
Voir sur GitHub
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Initialize Lucide icons
|
||||
lucide.createIcons();
|
||||
|
||||
// Smooth scrolling for navigation links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add interaction effects
|
||||
document.querySelectorAll('.card').forEach(card => {
|
||||
card.addEventListener('mouseenter', function() {
|
||||
this.style.transform = 'translateY(-4px)';
|
||||
});
|
||||
|
||||
card.addEventListener('mouseleave', function() {
|
||||
this.style.transform = 'translateY(0)';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,738 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Connexion - AperoNight | Plateforme Événementielle Premium</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="aperonight_premium_light_theme.css">
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans) !important;
|
||||
background: var(--background) !important;
|
||||
min-height: 100vh !important;
|
||||
position: relative !important;
|
||||
overflow-x: hidden !important;
|
||||
color: var(--foreground) !important;
|
||||
}
|
||||
|
||||
/* Light theme background patterns */
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
radial-gradient(circle at 2px 2px, var(--dot-color) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
opacity: 0.3;
|
||||
z-index: 0;
|
||||
animation: dotFlow 30s linear infinite;
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
linear-gradient(90deg, transparent 48%, var(--connection-color) 50%, transparent 52%),
|
||||
linear-gradient(0deg, transparent 48%, var(--connection-color) 50%, transparent 52%);
|
||||
background-size: 100px 100px;
|
||||
opacity: 0.12;
|
||||
z-index: 0;
|
||||
animation: connectionFlow 20s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes dotFlow {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(40px, 40px); }
|
||||
}
|
||||
|
||||
@keyframes connectionFlow {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(100px, 100px); }
|
||||
}
|
||||
|
||||
/* Page entrance orchestration */
|
||||
.page-container {
|
||||
animation: pageLoad 1000ms cubic-bezier(0.23, 1, 0.32, 1) forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(50px);
|
||||
}
|
||||
|
||||
@keyframes pageLoad {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Brand reveal animation */
|
||||
.brand-container {
|
||||
animation: brandReveal 1400ms ease-out 300ms forwards;
|
||||
opacity: 0;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
|
||||
@keyframes brandReveal {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Premium card elevation - light theme */
|
||||
.login-card {
|
||||
background: var(--glass-bg) !important;
|
||||
backdrop-filter: var(--glass-backdrop) !important;
|
||||
border: 1px solid var(--glass-border) !important;
|
||||
border-radius: var(--radius-2xl) !important;
|
||||
box-shadow: var(--shadow-2xl) !important;
|
||||
animation: cardElevate 800ms cubic-bezier(0.34, 1.56, 0.64, 1) 600ms forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(40px);
|
||||
transition: all 400ms ease-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(59, 130, 246, 0.05),
|
||||
transparent
|
||||
);
|
||||
transition: left 0.6s ease;
|
||||
}
|
||||
|
||||
.login-card:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.login-card:hover {
|
||||
transform: var(--hover-lift) var(--hover-scale);
|
||||
box-shadow: var(--shadow-2xl), var(--shadow-electric);
|
||||
}
|
||||
|
||||
@keyframes cardElevate {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Professional input styling - light theme */
|
||||
.input-group {
|
||||
position: relative;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
padding: 1.25rem 3.5rem 1.25rem 1.25rem;
|
||||
border: 2px solid var(--input-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--input);
|
||||
color: var(--card-foreground);
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
transition: all 250ms ease-out;
|
||||
outline: none;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: var(--shadow-electric), var(--focus-ring);
|
||||
transform: scale(1.01);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.input-field:focus + .floating-label {
|
||||
transform: translateY(-12px) scale(0.85);
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.floating-label {
|
||||
position: absolute;
|
||||
left: 1.25rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: var(--input);
|
||||
padding: 0 0.75rem;
|
||||
color: var(--muted-foreground);
|
||||
pointer-events: none;
|
||||
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-weight: 500;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.input-field:not(:placeholder-shown) + .floating-label {
|
||||
transform: translateY(-12px) scale(0.85);
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Security toggle with premium feel - light theme */
|
||||
.security-toggle {
|
||||
position: absolute;
|
||||
right: 1.25rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted-foreground);
|
||||
cursor: pointer;
|
||||
transition: all 200ms ease-out;
|
||||
opacity: 0.7;
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.security-toggle:hover {
|
||||
opacity: 1;
|
||||
color: var(--primary);
|
||||
background: var(--primary-light);
|
||||
transform: translateY(-50%) rotate(180deg);
|
||||
}
|
||||
|
||||
/* Flat button styling - light theme */
|
||||
.login-button {
|
||||
width: 100%;
|
||||
padding: 1.25rem;
|
||||
background: var(--primary) !important;
|
||||
border: none;
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--primary-foreground);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-display);
|
||||
cursor: pointer;
|
||||
transition: all 300ms ease-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
transform: var(--hover-lift);
|
||||
box-shadow: var(--shadow-lg);
|
||||
background: var(--primary-hover) !important;
|
||||
}
|
||||
|
||||
.login-button:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
/* Premium ripple effect */
|
||||
.login-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
transition: width 600ms cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||||
height 600ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.login-button:active::before {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
/* Sophisticated checkbox - light theme */
|
||||
.premium-checkbox {
|
||||
appearance: none;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 2px solid var(--input-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--input);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 200ms ease-out;
|
||||
}
|
||||
|
||||
.premium-checkbox:checked {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
animation: securityCheck 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
.premium-checkbox:checked::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 5px;
|
||||
width: 6px;
|
||||
height: 10px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
@keyframes securityCheck {
|
||||
0% { transform: scale(0); }
|
||||
50% { transform: scale(1.2); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Professional link styling - light theme */
|
||||
.premium-link {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
transition: all 250ms ease-out;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.premium-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
transition: width 250ms ease-out;
|
||||
}
|
||||
|
||||
.premium-link:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.premium-link:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Validation states - light theme */
|
||||
.input-error {
|
||||
border-color: var(--destructive) !important;
|
||||
animation: errorShake 400ms cubic-bezier(0.36, 0, 0.66, -0.56);
|
||||
}
|
||||
|
||||
@keyframes errorShake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-10px); }
|
||||
75% { transform: translateX(10px); }
|
||||
}
|
||||
|
||||
.input-success {
|
||||
border-color: var(--success) !important;
|
||||
box-shadow: 0 0 0 3px var(--success-light);
|
||||
animation: validationSuccess 500ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
@keyframes validationSuccess {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.02); border-color: var(--success); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Premium loading states - light theme */
|
||||
.skeleton {
|
||||
background: var(--muted);
|
||||
animation: skeletonPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes skeletonPulse {
|
||||
0%, 100% { opacity: 0.8; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
/* Logo styling - light theme */
|
||||
.logo-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logo-glow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Trust indicators - light theme */
|
||||
.trust-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--success-light);
|
||||
color: var(--success);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--success);
|
||||
transition: all 300ms ease-out;
|
||||
}
|
||||
|
||||
.trust-badge:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* Professional footer - light theme */
|
||||
.pro-footer {
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1rem;
|
||||
margin-top: 2rem;
|
||||
border: 1px solid rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Light theme brand colors */
|
||||
.brand-text-primary {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.brand-text-secondary {
|
||||
color: var(--primary-dark) !important;
|
||||
}
|
||||
|
||||
.brand-text-muted {
|
||||
color: var(--muted-foreground) !important;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: var(--primary) !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
opacity: 1;
|
||||
color: var(--primary-hover) !important;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: var(--muted-foreground) !important;
|
||||
}
|
||||
|
||||
/* Responsive enhancements */
|
||||
@media (max-width: 640px) {
|
||||
.login-card {
|
||||
margin: 1rem;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
padding: 1rem 3rem 1rem 1rem;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
font-size: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Advanced accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast mode support */
|
||||
@media (prefers-contrast: high) {
|
||||
.login-card {
|
||||
border: 3px solid var(--primary);
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
outline: 3px solid var(--primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-container relative z-10 flex items-center justify-center min-h-screen p-4">
|
||||
<div class="w-full max-w-lg">
|
||||
<!-- Premium Brand Section - Light Theme -->
|
||||
<div class="brand-container text-center mb-10">
|
||||
<div class="relative inline-block mb-6">
|
||||
<div class="logo-glow"></div>
|
||||
<div class="relative w-20 h-20 mx-auto bg-blue-600 rounded-2xl flex items-center justify-center logo-container">
|
||||
<i data-lucide="calendar-check" class="w-10 h-10 text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold brand-text-primary mb-2 font-display">AperoNight</h1>
|
||||
<p class="brand-text-secondary text-lg font-medium mb-2">Plateforme Événementielle Premium</p>
|
||||
<p class="brand-text-muted text-sm opacity-90">Connexion sécurisée • Interface professionnelle</p>
|
||||
|
||||
<div class="flex justify-center mt-4">
|
||||
<div class="trust-badge">
|
||||
<i data-lucide="shield-check" class="w-4 h-4"></i>
|
||||
<span>Connexion Sécurisée</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Premium Login Card -->
|
||||
<div class="login-card p-8">
|
||||
<div class="text-center mb-8">
|
||||
<h2 class="text-xl font-bold text-gray-800 mb-2 font-display">Accès Dashboard</h2>
|
||||
<p class="text-gray-600 text-sm">Gérez vos événements en toute simplicité</p>
|
||||
</div>
|
||||
|
||||
<form class="space-y-6">
|
||||
<!-- Email professionnel -->
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="email"
|
||||
class="input-field"
|
||||
placeholder=" "
|
||||
required
|
||||
id="email"
|
||||
>
|
||||
<label class="floating-label" for="email">Email professionnel</label>
|
||||
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<i data-lucide="mail" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mot de passe sécurisé -->
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="password"
|
||||
class="input-field"
|
||||
placeholder=" "
|
||||
required
|
||||
id="password"
|
||||
>
|
||||
<label class="floating-label" for="password">Mot de passe sécurisé</label>
|
||||
<button type="button" class="security-toggle" onclick="togglePassword()">
|
||||
<i data-lucide="lock" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Options de connexion -->
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center space-x-3 cursor-pointer group">
|
||||
<input type="checkbox" class="premium-checkbox" id="remember">
|
||||
<span class="text-sm text-gray-700 group-hover:text-gray-900 transition-colors">
|
||||
Maintenir la connexion
|
||||
</span>
|
||||
</label>
|
||||
<div class="flex items-center space-x-1 text-xs text-gray-500">
|
||||
<i data-lucide="timer" class="w-3 h-3"></i>
|
||||
<span>30 jours</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bouton de connexion premium -->
|
||||
<button type="submit" class="login-button group">
|
||||
<span class="relative z-10 flex items-center justify-center gap-2">
|
||||
<i data-lucide="log-in" class="w-5 h-5"></i>
|
||||
Accéder au Dashboard
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- Options de récupération -->
|
||||
<div class="text-center space-y-3">
|
||||
<a href="#" class="premium-link text-sm">Mot de passe oublié ?</a>
|
||||
<div class="flex items-center justify-center space-x-4 text-xs text-gray-500">
|
||||
<span class="flex items-center gap-1">
|
||||
<i data-lucide="smartphone" class="w-3 h-3"></i>
|
||||
2FA disponible
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<i data-lucide="key" class="w-3 h-3"></i>
|
||||
SSO Enterprise
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Professional Footer - Light Theme -->
|
||||
<div class="pro-footer text-center space-y-3">
|
||||
<div class="flex items-center justify-center space-x-6 text-sm">
|
||||
<a href="#" class="footer-link transition-colors flex items-center gap-1">
|
||||
<i data-lucide="life-buoy" class="w-4 h-4"></i>
|
||||
Support Pro
|
||||
</a>
|
||||
<a href="#" class="footer-link transition-colors flex items-center gap-1">
|
||||
<i data-lucide="shield" class="w-4 h-4"></i>
|
||||
Sécurité Renforcée
|
||||
</a>
|
||||
<a href="#" class="footer-link transition-colors flex items-center gap-1">
|
||||
<i data-lucide="zap" class="w-4 h-4"></i>
|
||||
API Premium
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-xs status-text">© 2024 AperoNight • Plateforme Événementielle Premium • Tous droits réservés</p>
|
||||
<div class="flex items-center justify-center space-x-2 text-xs status-text">
|
||||
<span class="flex items-center gap-1">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
Système opérationnel
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span>99.9% uptime</span>
|
||||
<span>•</span>
|
||||
<span>GDPR compliant</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize Lucide icons
|
||||
lucide.createIcons();
|
||||
|
||||
// Enhanced password toggle
|
||||
function togglePassword() {
|
||||
const passwordField = document.getElementById('password');
|
||||
const toggleIcon = document.querySelector('.security-toggle i');
|
||||
|
||||
if (passwordField.type === 'password') {
|
||||
passwordField.type = 'text';
|
||||
toggleIcon.setAttribute('data-lucide', 'unlock');
|
||||
} else {
|
||||
passwordField.type = 'password';
|
||||
toggleIcon.setAttribute('data-lucide', 'lock');
|
||||
}
|
||||
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// Professional form validation
|
||||
const form = document.querySelector('form');
|
||||
const emailField = document.getElementById('email');
|
||||
const passwordField = document.getElementById('password');
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Reset states
|
||||
emailField.classList.remove('input-error', 'input-success');
|
||||
passwordField.classList.remove('input-error', 'input-success');
|
||||
|
||||
let isValid = true;
|
||||
|
||||
// Professional email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(emailField.value)) {
|
||||
emailField.classList.add('input-error');
|
||||
isValid = false;
|
||||
showNotification('Email invalide', 'error');
|
||||
} else {
|
||||
emailField.classList.add('input-success');
|
||||
}
|
||||
|
||||
// Secure password validation
|
||||
if (passwordField.value.length < 8) {
|
||||
passwordField.classList.add('input-error');
|
||||
isValid = false;
|
||||
showNotification('Mot de passe trop court (min. 8 caractères)', 'error');
|
||||
} else {
|
||||
passwordField.classList.add('input-success');
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
// Premium loading state
|
||||
const button = document.querySelector('.login-button');
|
||||
const originalContent = button.innerHTML;
|
||||
button.innerHTML = `
|
||||
<div class="flex items-center justify-center space-x-2">
|
||||
<div class="animate-spin w-5 h-5 border-2 border-white border-t-transparent rounded-full"></div>
|
||||
<span>Connexion sécurisée...</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
setTimeout(() => {
|
||||
showNotification('Connexion réussie ! Redirection...', 'success');
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalContent;
|
||||
}, 1500);
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
// Real-time validation
|
||||
emailField.addEventListener('input', function() {
|
||||
this.classList.remove('input-error', 'input-success');
|
||||
});
|
||||
|
||||
passwordField.addEventListener('input', function() {
|
||||
this.classList.remove('input-error', 'input-success');
|
||||
});
|
||||
|
||||
// Professional notification system
|
||||
function showNotification(message, type) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `
|
||||
fixed top-4 right-4 z-50 p-4 rounded-lg shadow-2xl max-w-sm
|
||||
${type === 'success' ? 'bg-green-500 text-white' : 'bg-red-500 text-white'}
|
||||
transform transition-all duration-300 ease-out translate-x-full
|
||||
`;
|
||||
notification.innerHTML = `
|
||||
<div class="flex items-center space-x-2">
|
||||
<i data-lucide="${type === 'success' ? 'check-circle' : 'alert-circle'}" class="w-5 h-5"></i>
|
||||
<span class="font-medium">${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
lucide.createIcons();
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(0)';
|
||||
}, 100);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(notification);
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Enhanced floating label behavior
|
||||
document.querySelectorAll('.input-field').forEach(input => {
|
||||
input.addEventListener('focus', function() {
|
||||
this.nextElementSibling.style.background = 'white';
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function() {
|
||||
if (!this.value) {
|
||||
this.nextElementSibling.style.background = 'var(--input)';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Professional interaction tracking
|
||||
console.log('🌟 AperoNight Premium Light Login Interface Loaded');
|
||||
console.log('✅ Security features: 2FA, SSO, GDPR compliance');
|
||||
console.log('🎨 Theme: Professional Event Platform - Light Mode');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,125 +0,0 @@
|
||||
:root {
|
||||
/* AperoNight Premium Light Theme - Professional Event Platform */
|
||||
|
||||
/* Base Colors - Clean Light Background with Professional Accents */
|
||||
--background: oklch(0.9800 0.0050 240);
|
||||
--foreground: oklch(0.1500 0.0200 240);
|
||||
--surface: oklch(0.9600 0.0080 240);
|
||||
--surface-elevated: oklch(0.9400 0.0120 240);
|
||||
|
||||
/* Card & Dialog surfaces */
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0.1500 0.0200 240);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0.1500 0.0200 240);
|
||||
|
||||
/* Primary - Professional Electric Blue */
|
||||
--primary: oklch(0.5200 0.2200 220);
|
||||
--primary-foreground: oklch(0.9900 0.0050 220);
|
||||
--primary-hover: oklch(0.4600 0.2400 220);
|
||||
--primary-light: oklch(0.9200 0.1000 220);
|
||||
--primary-dark: oklch(0.3800 0.2600 220);
|
||||
|
||||
/* Secondary - Sophisticated Light Gray */
|
||||
--secondary: oklch(0.9200 0.0100 240);
|
||||
--secondary-foreground: oklch(0.3000 0.0300 240);
|
||||
--secondary-hover: oklch(0.8800 0.0150 240);
|
||||
|
||||
/* Accent - Vibrant Cyan (Events Energy) */
|
||||
--accent: oklch(0.6500 0.2400 200);
|
||||
--accent-foreground: oklch(0.9900 0.0050 200);
|
||||
--accent-light: oklch(0.9400 0.1200 200);
|
||||
--accent-dark: oklch(0.5000 0.2800 200);
|
||||
|
||||
/* Success - Event Success Green */
|
||||
--success: oklch(0.6000 0.2000 140);
|
||||
--success-foreground: oklch(0.9800 0.0100 140);
|
||||
--success-light: oklch(0.9600 0.0800 140);
|
||||
|
||||
/* Warning - Premium Amber */
|
||||
--warning: oklch(0.7200 0.1800 60);
|
||||
--warning-foreground: oklch(0.2500 0.0400 60);
|
||||
--warning-light: oklch(0.9600 0.0800 60);
|
||||
|
||||
/* Error - Professional Red */
|
||||
--destructive: oklch(0.5600 0.2200 20);
|
||||
--destructive-foreground: oklch(0.9800 0.0100 20);
|
||||
--destructive-light: oklch(0.9600 0.1000 20);
|
||||
|
||||
/* Muted tones */
|
||||
--muted: oklch(0.9400 0.0100 240);
|
||||
--muted-foreground: oklch(0.5200 0.0300 240);
|
||||
--muted-dark: oklch(0.8800 0.0200 240);
|
||||
|
||||
/* Borders and inputs */
|
||||
--border: oklch(0.8800 0.0200 240);
|
||||
--input: oklch(0.9800 0.0080 240);
|
||||
--input-border: oklch(0.8600 0.0300 240);
|
||||
--ring: oklch(0.5200 0.2200 220);
|
||||
|
||||
/* Typography - Premium Event Platform */
|
||||
--font-sans: 'Inter', 'Plus Jakarta Sans', system-ui, sans-serif;
|
||||
--font-display: 'Space Grotesk', 'Outfit', system-ui, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
|
||||
/* Spacing and layout */
|
||||
--radius: 0.75rem;
|
||||
--spacing: 1rem;
|
||||
|
||||
/* Light theme shadow system */
|
||||
--shadow-xs: 0 1px 3px 0 hsl(240 15% 15% / 0.08), 0 1px 2px -1px hsl(240 15% 15% / 0.06);
|
||||
--shadow-sm: 0 2px 6px -1px hsl(240 15% 15% / 0.10), 0 2px 4px -2px hsl(240 15% 15% / 0.08);
|
||||
--shadow: 0 4px 8px -2px hsl(240 15% 15% / 0.12), 0 2px 4px -2px hsl(240 15% 15% / 0.08);
|
||||
--shadow-md: 0 8px 16px -4px hsl(240 15% 15% / 0.14), 0 4px 6px -2px hsl(240 15% 15% / 0.10);
|
||||
--shadow-lg: 0 16px 24px -4px hsl(240 15% 15% / 0.16), 0 8px 8px -4px hsl(240 15% 15% / 0.08);
|
||||
--shadow-xl: 0 20px 32px -8px hsl(240 15% 15% / 0.18), 0 8px 16px -8px hsl(240 15% 15% / 0.10);
|
||||
--shadow-2xl: 0 32px 64px -12px hsl(240 15% 15% / 0.22);
|
||||
|
||||
/* Subtle accent shadows for light theme */
|
||||
--shadow-electric: 0 4px 16px -2px hsl(220 80% 60% / 0.15), 0 2px 8px -2px hsl(220 80% 60% / 0.10);
|
||||
--shadow-accent: 0 4px 16px -2px hsl(200 80% 60% / 0.18), 0 2px 8px -2px hsl(200 80% 60% / 0.12);
|
||||
|
||||
/* Light theme gradients */
|
||||
--gradient-primary: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
||||
--gradient-background: linear-gradient(135deg,
|
||||
oklch(0.9800 0.0050 240) 0%,
|
||||
oklch(0.9600 0.0080 235) 25%,
|
||||
oklch(0.9400 0.0120 230) 50%,
|
||||
oklch(0.9600 0.0080 225) 75%,
|
||||
oklch(0.9800 0.0050 220) 100%);
|
||||
--gradient-card: linear-gradient(135deg,
|
||||
oklch(1.0000 0 0) 0%,
|
||||
oklch(0.9900 0.0050 235) 100%);
|
||||
|
||||
/* Light theme pattern overlays */
|
||||
--grid-color: oklch(0.8500 0.0300 240);
|
||||
--dot-color: oklch(0.8000 0.0400 220);
|
||||
--connection-color: oklch(0.7500 0.0800 210);
|
||||
|
||||
/* Light glassmorphism */
|
||||
--glass-bg: oklch(1.0000 0 0 / 0.85);
|
||||
--glass-border: oklch(0.8800 0.0200 240 / 0.25);
|
||||
--glass-backdrop: blur(16px) saturate(180%);
|
||||
|
||||
/* Professional interaction states */
|
||||
--hover-lift: translateY(-2px);
|
||||
--hover-scale: scale(1.02);
|
||||
--focus-ring: 0 0 0 3px var(--ring);
|
||||
|
||||
/* Event-specific colors for light theme */
|
||||
--event-vip: oklch(0.6800 0.2200 45);
|
||||
--event-premium: oklch(0.5800 0.2000 280);
|
||||
--event-standard: oklch(0.6200 0.1600 160);
|
||||
--event-available: oklch(0.6000 0.1800 140);
|
||||
--event-limited: oklch(0.7000 0.1800 50);
|
||||
--event-sold-out: oklch(0.5800 0.2000 15);
|
||||
|
||||
/* Radius variations */
|
||||
--radius-xs: calc(var(--radius) - 4px);
|
||||
--radius-sm: calc(var(--radius) - 2px);
|
||||
--radius-md: var(--radius);
|
||||
--radius-lg: calc(var(--radius) + 4px);
|
||||
--radius-xl: calc(var(--radius) + 8px);
|
||||
--radius-2xl: calc(var(--radius) + 12px);
|
||||
--radius-full: 9999px;
|
||||
}
|
||||
@@ -1,710 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Connexion - AperoNight | Plateforme Événementielle Premium</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="aperonight_premium_theme.css">
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans) !important;
|
||||
background: oklch(0.1200 0.0300 240) !important;
|
||||
min-height: 100vh !important;
|
||||
position: relative !important;
|
||||
overflow-x: hidden !important;
|
||||
color: var(--foreground) !important;
|
||||
}
|
||||
|
||||
/* Advanced background patterns */
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
radial-gradient(circle at 2px 2px, var(--dot-color) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
opacity: 0.4;
|
||||
z-index: 0;
|
||||
animation: dotFlow 30s linear infinite;
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
linear-gradient(90deg, transparent 48%, var(--connection-color) 50%, transparent 52%),
|
||||
linear-gradient(0deg, transparent 48%, var(--connection-color) 50%, transparent 52%);
|
||||
background-size: 100px 100px;
|
||||
opacity: 0.15;
|
||||
z-index: 0;
|
||||
animation: connectionFlow 20s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes dotFlow {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(40px, 40px); }
|
||||
}
|
||||
|
||||
@keyframes connectionFlow {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(100px, 100px); }
|
||||
}
|
||||
|
||||
/* Page entrance orchestration */
|
||||
.page-container {
|
||||
animation: pageLoad 1000ms cubic-bezier(0.23, 1, 0.32, 1) forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(50px);
|
||||
}
|
||||
|
||||
@keyframes pageLoad {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Brand reveal animation */
|
||||
.brand-container {
|
||||
animation: brandReveal 1400ms ease-out 300ms forwards;
|
||||
opacity: 0;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
|
||||
@keyframes brandReveal {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Premium card elevation */
|
||||
.login-card {
|
||||
background: var(--glass-bg) !important;
|
||||
backdrop-filter: var(--glass-backdrop) !important;
|
||||
border: 1px solid var(--glass-border) !important;
|
||||
border-radius: var(--radius-2xl) !important;
|
||||
box-shadow: var(--shadow-2xl) !important;
|
||||
animation: cardElevate 800ms cubic-bezier(0.34, 1.56, 0.64, 1) 600ms forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(40px);
|
||||
transition: all 400ms ease-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.1),
|
||||
transparent
|
||||
);
|
||||
transition: left 0.6s ease;
|
||||
}
|
||||
|
||||
.login-card:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.login-card:hover {
|
||||
transform: var(--hover-lift) var(--hover-scale);
|
||||
box-shadow: var(--shadow-2xl), var(--shadow-electric);
|
||||
}
|
||||
|
||||
@keyframes cardElevate {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Professional input styling */
|
||||
.input-group {
|
||||
position: relative;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
padding: 1.25rem 3.5rem 1.25rem 1.25rem;
|
||||
border: 2px solid var(--input-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--input);
|
||||
color: var(--card-foreground);
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
transition: all 250ms ease-out;
|
||||
outline: none;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: var(--shadow-electric), var(--focus-ring);
|
||||
transform: scale(1.01);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.input-field:focus + .floating-label {
|
||||
transform: translateY(-12px) scale(0.85);
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.floating-label {
|
||||
position: absolute;
|
||||
left: 1.25rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: var(--input);
|
||||
padding: 0 0.75rem;
|
||||
color: var(--muted-foreground);
|
||||
pointer-events: none;
|
||||
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-weight: 500;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.input-field:not(:placeholder-shown) + .floating-label {
|
||||
transform: translateY(-12px) scale(0.85);
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Security toggle with premium feel */
|
||||
.security-toggle {
|
||||
position: absolute;
|
||||
right: 1.25rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted-foreground);
|
||||
cursor: pointer;
|
||||
transition: all 200ms ease-out;
|
||||
opacity: 0.7;
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.security-toggle:hover {
|
||||
opacity: 1;
|
||||
color: var(--primary);
|
||||
background: var(--primary-light);
|
||||
transform: translateY(-50%) rotate(180deg);
|
||||
}
|
||||
|
||||
/* Flat button styling */
|
||||
.login-button {
|
||||
width: 100%;
|
||||
padding: 1.25rem;
|
||||
background: var(--primary) !important;
|
||||
border: none;
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--primary-foreground);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-display);
|
||||
cursor: pointer;
|
||||
transition: all 300ms ease-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
transform: var(--hover-lift);
|
||||
box-shadow: var(--shadow-lg);
|
||||
background: var(--primary-hover) !important;
|
||||
}
|
||||
|
||||
.login-button:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
/* Premium ripple effect */
|
||||
.login-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
transition: width 600ms cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||||
height 600ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.login-button:active::before {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
/* Sophisticated checkbox */
|
||||
.premium-checkbox {
|
||||
appearance: none;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 2px solid var(--input-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--input);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 200ms ease-out;
|
||||
}
|
||||
|
||||
.premium-checkbox:checked {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
animation: securityCheck 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
.premium-checkbox:checked::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 5px;
|
||||
width: 6px;
|
||||
height: 10px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
@keyframes securityCheck {
|
||||
0% { transform: scale(0); }
|
||||
50% { transform: scale(1.2); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Professional link styling */
|
||||
.premium-link {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
transition: all 250ms ease-out;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.premium-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
transition: width 250ms ease-out;
|
||||
}
|
||||
|
||||
.premium-link:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.premium-link:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Validation states */
|
||||
.input-error {
|
||||
border-color: var(--destructive) !important;
|
||||
animation: errorShake 400ms cubic-bezier(0.36, 0, 0.66, -0.56);
|
||||
}
|
||||
|
||||
@keyframes errorShake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-10px); }
|
||||
75% { transform: translateX(10px); }
|
||||
}
|
||||
|
||||
.input-success {
|
||||
border-color: var(--success) !important;
|
||||
box-shadow: 0 0 0 3px var(--success-light);
|
||||
animation: validationSuccess 500ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
@keyframes validationSuccess {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.02); border-color: var(--success); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Premium loading states */
|
||||
.skeleton {
|
||||
background: var(--muted);
|
||||
animation: skeletonPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes skeletonPulse {
|
||||
0%, 100% { opacity: 0.8; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
/* Logo styling */
|
||||
.logo-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logo-glow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Trust indicators */
|
||||
.trust-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--success-light);
|
||||
color: var(--success);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--success);
|
||||
transition: all 300ms ease-out;
|
||||
}
|
||||
|
||||
.trust-badge:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* Professional footer */
|
||||
.pro-footer {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
/* Responsive enhancements */
|
||||
@media (max-width: 640px) {
|
||||
.login-card {
|
||||
margin: 1rem;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
padding: 1rem 3rem 1rem 1rem;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
font-size: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Advanced accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast mode support */
|
||||
@media (prefers-contrast: high) {
|
||||
.login-card {
|
||||
border: 3px solid var(--primary);
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
outline: 3px solid var(--primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-container relative z-10 flex items-center justify-center min-h-screen p-4">
|
||||
<div class="w-full max-w-lg">
|
||||
<!-- Premium Brand Section -->
|
||||
<div class="brand-container text-center mb-10">
|
||||
<div class="relative inline-block mb-6">
|
||||
<div class="logo-glow"></div>
|
||||
<div class="relative w-20 h-20 mx-auto bg-blue-600 rounded-2xl flex items-center justify-center logo-container">
|
||||
<i data-lucide="calendar-check" class="w-10 h-10 text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2 font-display">AperoNight</h1>
|
||||
<p class="text-blue-200 text-lg font-medium mb-2">Plateforme Événementielle Premium</p>
|
||||
<p class="text-blue-300 text-sm opacity-90">Connexion sécurisée • Interface professionnelle</p>
|
||||
|
||||
<div class="flex justify-center mt-4">
|
||||
<div class="trust-badge">
|
||||
<i data-lucide="shield-check" class="w-4 h-4"></i>
|
||||
<span>Connexion Sécurisée</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Premium Login Card -->
|
||||
<div class="login-card p-8">
|
||||
<div class="text-center mb-8">
|
||||
<h2 class="text-xl font-bold text-gray-800 mb-2 font-display">Accès Dashboard</h2>
|
||||
<p class="text-gray-600 text-sm">Gérez vos événements en toute simplicité</p>
|
||||
</div>
|
||||
|
||||
<form class="space-y-6">
|
||||
<!-- Email professionnel -->
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="email"
|
||||
class="input-field"
|
||||
placeholder=" "
|
||||
required
|
||||
id="email"
|
||||
>
|
||||
<label class="floating-label" for="email">Email professionnel</label>
|
||||
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<i data-lucide="mail" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mot de passe sécurisé -->
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="password"
|
||||
class="input-field"
|
||||
placeholder=" "
|
||||
required
|
||||
id="password"
|
||||
>
|
||||
<label class="floating-label" for="password">Mot de passe sécurisé</label>
|
||||
<button type="button" class="security-toggle" onclick="togglePassword()">
|
||||
<i data-lucide="lock" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Options de connexion -->
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center space-x-3 cursor-pointer group">
|
||||
<input type="checkbox" class="premium-checkbox" id="remember">
|
||||
<span class="text-sm text-gray-700 group-hover:text-gray-900 transition-colors">
|
||||
Maintenir la connexion
|
||||
</span>
|
||||
</label>
|
||||
<div class="flex items-center space-x-1 text-xs text-gray-500">
|
||||
<i data-lucide="timer" class="w-3 h-3"></i>
|
||||
<span>30 jours</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bouton de connexion premium -->
|
||||
<button type="submit" class="login-button group">
|
||||
<span class="relative z-10 flex items-center justify-center gap-2">
|
||||
<i data-lucide="log-in" class="w-5 h-5"></i>
|
||||
Accéder au Dashboard
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- Options de récupération -->
|
||||
<div class="text-center space-y-3">
|
||||
<a href="#" class="premium-link text-sm">Mot de passe oublié ?</a>
|
||||
<div class="flex items-center justify-center space-x-4 text-xs text-gray-500">
|
||||
<span class="flex items-center gap-1">
|
||||
<i data-lucide="smartphone" class="w-3 h-3"></i>
|
||||
2FA disponible
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<i data-lucide="key" class="w-3 h-3"></i>
|
||||
SSO Enterprise
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Professional Footer -->
|
||||
<div class="pro-footer text-center space-y-3">
|
||||
<div class="flex items-center justify-center space-x-6 text-sm text-blue-200">
|
||||
<a href="#" class="hover:text-white transition-colors flex items-center gap-1">
|
||||
<i data-lucide="life-buoy" class="w-4 h-4"></i>
|
||||
Support Pro
|
||||
</a>
|
||||
<a href="#" class="hover:text-white transition-colors flex items-center gap-1">
|
||||
<i data-lucide="shield" class="w-4 h-4"></i>
|
||||
Sécurité Renforcée
|
||||
</a>
|
||||
<a href="#" class="hover:text-white transition-colors flex items-center gap-1">
|
||||
<i data-lucide="zap" class="w-4 h-4"></i>
|
||||
API Premium
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-xs text-blue-300">© 2024 AperoNight • Plateforme Événementielle Premium • Tous droits réservés</p>
|
||||
<div class="flex items-center justify-center space-x-2 text-xs text-blue-400">
|
||||
<span class="flex items-center gap-1">
|
||||
<div class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
|
||||
Système opérationnel
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span>99.9% uptime</span>
|
||||
<span>•</span>
|
||||
<span>GDPR compliant</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize Lucide icons
|
||||
lucide.createIcons();
|
||||
|
||||
// Enhanced password toggle
|
||||
function togglePassword() {
|
||||
const passwordField = document.getElementById('password');
|
||||
const toggleIcon = document.querySelector('.security-toggle i');
|
||||
|
||||
if (passwordField.type === 'password') {
|
||||
passwordField.type = 'text';
|
||||
toggleIcon.setAttribute('data-lucide', 'unlock');
|
||||
} else {
|
||||
passwordField.type = 'password';
|
||||
toggleIcon.setAttribute('data-lucide', 'lock');
|
||||
}
|
||||
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// Professional form validation
|
||||
const form = document.querySelector('form');
|
||||
const emailField = document.getElementById('email');
|
||||
const passwordField = document.getElementById('password');
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Reset states
|
||||
emailField.classList.remove('input-error', 'input-success');
|
||||
passwordField.classList.remove('input-error', 'input-success');
|
||||
|
||||
let isValid = true;
|
||||
|
||||
// Professional email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(emailField.value)) {
|
||||
emailField.classList.add('input-error');
|
||||
isValid = false;
|
||||
showNotification('Email invalide', 'error');
|
||||
} else {
|
||||
emailField.classList.add('input-success');
|
||||
}
|
||||
|
||||
// Secure password validation
|
||||
if (passwordField.value.length < 8) {
|
||||
passwordField.classList.add('input-error');
|
||||
isValid = false;
|
||||
showNotification('Mot de passe trop court (min. 8 caractères)', 'error');
|
||||
} else {
|
||||
passwordField.classList.add('input-success');
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
// Premium loading state
|
||||
const button = document.querySelector('.login-button');
|
||||
const originalContent = button.innerHTML;
|
||||
button.innerHTML = `
|
||||
<div class="flex items-center justify-center space-x-2">
|
||||
<div class="animate-spin w-5 h-5 border-2 border-white border-t-transparent rounded-full"></div>
|
||||
<span>Connexion sécurisée...</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
setTimeout(() => {
|
||||
showNotification('Connexion réussie ! Redirection...', 'success');
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalContent;
|
||||
}, 1500);
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
// Real-time validation
|
||||
emailField.addEventListener('input', function() {
|
||||
this.classList.remove('input-error', 'input-success');
|
||||
});
|
||||
|
||||
passwordField.addEventListener('input', function() {
|
||||
this.classList.remove('input-error', 'input-success');
|
||||
});
|
||||
|
||||
// Professional notification system
|
||||
function showNotification(message, type) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `
|
||||
fixed top-4 right-4 z-50 p-4 rounded-lg shadow-2xl max-w-sm
|
||||
${type === 'success' ? 'bg-green-500 text-white' : 'bg-red-500 text-white'}
|
||||
transform transition-all duration-300 ease-out translate-x-full
|
||||
`;
|
||||
notification.innerHTML = `
|
||||
<div class="flex items-center space-x-2">
|
||||
<i data-lucide="${type === 'success' ? 'check-circle' : 'alert-circle'}" class="w-5 h-5"></i>
|
||||
<span class="font-medium">${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
lucide.createIcons();
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(0)';
|
||||
}, 100);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(notification);
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Enhanced floating label behavior
|
||||
document.querySelectorAll('.input-field').forEach(input => {
|
||||
input.addEventListener('focus', function() {
|
||||
this.nextElementSibling.style.background = 'white';
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function() {
|
||||
if (!this.value) {
|
||||
this.nextElementSibling.style.background = 'var(--input)';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Professional interaction tracking
|
||||
console.log('🚀 AperoNight Premium Login Interface Loaded');
|
||||
console.log('✅ Security features: 2FA, SSO, GDPR compliance');
|
||||
console.log('🎨 Theme: Professional Event Platform');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,125 +0,0 @@
|
||||
:root {
|
||||
/* AperoNight Premium Theme - Telecom Inspired */
|
||||
|
||||
/* Base Colors - Sophisticated Navy & Electric Accents */
|
||||
--background: oklch(0.1200 0.0300 240);
|
||||
--foreground: oklch(0.9500 0.0100 240);
|
||||
--surface: oklch(0.1600 0.0400 240);
|
||||
--surface-elevated: oklch(0.2000 0.0500 240);
|
||||
|
||||
/* Card & Dialog surfaces */
|
||||
--card: oklch(0.9800 0.0100 240);
|
||||
--card-foreground: oklch(0.1500 0.0200 240);
|
||||
--popover: oklch(0.9800 0.0100 240);
|
||||
--popover-foreground: oklch(0.1500 0.0200 240);
|
||||
|
||||
/* Primary - Premium Electric Blue */
|
||||
--primary: oklch(0.5500 0.2400 220);
|
||||
--primary-foreground: oklch(0.9800 0.0100 220);
|
||||
--primary-hover: oklch(0.4800 0.2600 220);
|
||||
--primary-light: oklch(0.8500 0.1200 220);
|
||||
--primary-dark: oklch(0.3500 0.2800 220);
|
||||
|
||||
/* Secondary - Sophisticated Slate */
|
||||
--secondary: oklch(0.8800 0.0200 240);
|
||||
--secondary-foreground: oklch(0.2500 0.0300 240);
|
||||
--secondary-hover: oklch(0.8200 0.0300 240);
|
||||
|
||||
/* Accent - Vibrant Cyan (Events Energy) */
|
||||
--accent: oklch(0.6800 0.2600 200);
|
||||
--accent-foreground: oklch(0.9800 0.0100 200);
|
||||
--accent-light: oklch(0.8800 0.1400 200);
|
||||
--accent-dark: oklch(0.4500 0.3000 200);
|
||||
|
||||
/* Success - Event Success Green */
|
||||
--success: oklch(0.6200 0.2200 140);
|
||||
--success-foreground: oklch(0.9600 0.0200 140);
|
||||
--success-light: oklch(0.9200 0.1000 140);
|
||||
|
||||
/* Warning - Premium Gold */
|
||||
--warning: oklch(0.7500 0.2000 60);
|
||||
--warning-foreground: oklch(0.2000 0.0300 60);
|
||||
--warning-light: oklch(0.9400 0.1000 60);
|
||||
|
||||
/* Error - Professional Red */
|
||||
--destructive: oklch(0.5800 0.2400 20);
|
||||
--destructive-foreground: oklch(0.9700 0.0200 20);
|
||||
--destructive-light: oklch(0.9300 0.1200 20);
|
||||
|
||||
/* Muted tones */
|
||||
--muted: oklch(0.8800 0.0200 240);
|
||||
--muted-foreground: oklch(0.4800 0.0400 240);
|
||||
--muted-dark: oklch(0.7500 0.0300 240);
|
||||
|
||||
/* Borders and inputs */
|
||||
--border: oklch(0.8400 0.0300 240);
|
||||
--input: oklch(0.9600 0.0200 240);
|
||||
--input-border: oklch(0.8200 0.0400 240);
|
||||
--ring: oklch(0.5500 0.2400 220);
|
||||
|
||||
/* Typography - Premium Event Platform */
|
||||
--font-sans: 'Inter', 'Plus Jakarta Sans', system-ui, sans-serif;
|
||||
--font-display: 'Space Grotesk', 'Outfit', system-ui, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
|
||||
/* Spacing and layout */
|
||||
--radius: 0.75rem;
|
||||
--spacing: 1rem;
|
||||
|
||||
/* Premium shadow system */
|
||||
--shadow-xs: 0 1px 3px 0 hsl(240 30% 8% / 0.08), 0 1px 2px -1px hsl(240 30% 8% / 0.06);
|
||||
--shadow-sm: 0 2px 6px -1px hsl(240 30% 8% / 0.10), 0 2px 4px -2px hsl(240 30% 8% / 0.08);
|
||||
--shadow: 0 4px 8px -2px hsl(240 30% 8% / 0.12), 0 2px 4px -2px hsl(240 30% 8% / 0.08);
|
||||
--shadow-md: 0 8px 16px -4px hsl(240 30% 8% / 0.14), 0 4px 6px -2px hsl(240 30% 8% / 0.10);
|
||||
--shadow-lg: 0 16px 24px -4px hsl(240 30% 8% / 0.16), 0 8px 8px -4px hsl(240 30% 8% / 0.08);
|
||||
--shadow-xl: 0 20px 32px -8px hsl(240 30% 8% / 0.20), 0 8px 16px -8px hsl(240 30% 8% / 0.12);
|
||||
--shadow-2xl: 0 32px 64px -12px hsl(240 30% 8% / 0.25);
|
||||
|
||||
/* Electric/Glow shadows for premium effects */
|
||||
--shadow-electric: 0 4px 16px -2px hsl(220 100% 70% / 0.20), 0 2px 8px -2px hsl(220 100% 70% / 0.15);
|
||||
--shadow-accent: 0 4px 16px -2px hsl(200 100% 70% / 0.25), 0 2px 8px -2px hsl(200 100% 70% / 0.20);
|
||||
|
||||
/* Premium gradients */
|
||||
--gradient-primary: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
||||
--gradient-background: linear-gradient(135deg,
|
||||
oklch(0.1200 0.0300 240) 0%,
|
||||
oklch(0.1000 0.0400 235) 25%,
|
||||
oklch(0.0800 0.0500 230) 50%,
|
||||
oklch(0.1000 0.0400 225) 75%,
|
||||
oklch(0.1200 0.0300 220) 100%);
|
||||
--gradient-card: linear-gradient(135deg,
|
||||
oklch(0.9900 0.0100 240) 0%,
|
||||
oklch(0.9700 0.0200 235) 100%);
|
||||
|
||||
/* Tech pattern overlays */
|
||||
--grid-color: oklch(0.3000 0.0500 240);
|
||||
--dot-color: oklch(0.2500 0.0600 220);
|
||||
--connection-color: oklch(0.4000 0.1200 210);
|
||||
|
||||
/* Glass morphism for premium feel */
|
||||
--glass-bg: oklch(0.9800 0.0100 240 / 0.80);
|
||||
--glass-border: oklch(0.8500 0.0300 240 / 0.30);
|
||||
--glass-backdrop: blur(16px) saturate(200%);
|
||||
|
||||
/* Professional states */
|
||||
--hover-lift: translateY(-2px);
|
||||
--hover-scale: scale(1.02);
|
||||
--focus-ring: 0 0 0 3px var(--ring);
|
||||
|
||||
/* Event-specific colors */
|
||||
--event-vip: oklch(0.6500 0.2500 45);
|
||||
--event-premium: oklch(0.5500 0.2200 280);
|
||||
--event-standard: oklch(0.6000 0.1800 160);
|
||||
--event-available: oklch(0.6200 0.2000 140);
|
||||
--event-limited: oklch(0.7200 0.2000 50);
|
||||
--event-sold-out: oklch(0.5500 0.2200 15);
|
||||
|
||||
/* Radius variations */
|
||||
--radius-xs: calc(var(--radius) - 4px);
|
||||
--radius-sm: calc(var(--radius) - 2px);
|
||||
--radius-md: var(--radius);
|
||||
--radius-lg: calc(var(--radius) + 4px);
|
||||
--radius-xl: calc(var(--radius) + 8px);
|
||||
--radius-2xl: calc(var(--radius) + 12px);
|
||||
--radius-full: 9999px;
|
||||
}
|
||||
@@ -1,385 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard - Minimalist Typography Design</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary: #1a1a1a;
|
||||
--secondary: #6b7280;
|
||||
--accent: #3b82f6;
|
||||
--background: #fafafa;
|
||||
--surface: #ffffff;
|
||||
--border: #e5e7eb;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: var(--background);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.mono { font-family: 'JetBrains Mono', monospace; }
|
||||
|
||||
.minimal-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.minimal-card:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.metric-number {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.text-subtle { color: var(--secondary); }
|
||||
.bg-subtle { background-color: #f8fafc; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="min-h-screen">
|
||||
<!-- Navigation -->
|
||||
<nav class="border-b border-gray-200 bg-white">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex items-center space-x-8">
|
||||
<h1 class="text-xl font-semibold">ApéroNight</h1>
|
||||
<div class="flex space-x-6">
|
||||
<a href="#" class="text-gray-900 border-b-2 border-blue-500 pb-1">Dashboard</a>
|
||||
<a href="#" class="text-gray-500 hover:text-gray-900">Événements</a>
|
||||
<a href="#" class="text-gray-500 hover:text-gray-900">Profil</a>
|
||||
</div>
|
||||
</div>
|
||||
<button class="p-2 rounded-lg hover:bg-gray-100">
|
||||
<i data-lucide="bell" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-12 fade-in">
|
||||
<h1 class="text-4xl font-bold mb-2">Bonjour, Marie</h1>
|
||||
<p class="text-lg text-subtle">Voici un aperçu de vos activités et événements</p>
|
||||
</div>
|
||||
|
||||
<!-- Critical Alert - Draft Tickets -->
|
||||
<div class="minimal-card rounded-lg p-6 mb-8 border-l-4 border-orange-400 bg-orange-50 fade-in">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="p-2 bg-orange-100 rounded-lg">
|
||||
<i data-lucide="clock" class="w-5 h-5 text-orange-600"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 mb-1">Action requise</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">2 billets en attente de paiement expirent dans 25 minutes</p>
|
||||
|
||||
<!-- Ticket Details -->
|
||||
<div class="bg-white rounded-lg p-3 mb-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<span class="font-medium text-sm">Soirée Jazz au Sunset</span>
|
||||
<span class="text-xs text-gray-500 ml-2">2 billets • €70</span>
|
||||
</div>
|
||||
<span class="mono text-xs bg-orange-100 text-orange-800 px-2 py-1 rounded">1/3 tentatives</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="bg-orange-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-orange-700 transition-colors">
|
||||
Payer maintenant
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metrics Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-12 fade-in">
|
||||
<div class="minimal-card rounded-lg p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<span class="text-sm font-medium text-subtle">Réservations</span>
|
||||
<i data-lucide="calendar-check" class="w-4 h-4 text-green-500"></i>
|
||||
</div>
|
||||
<div class="metric-number text-3xl text-gray-900 mb-1">05</div>
|
||||
<div class="text-xs text-subtle">+2 ce mois</div>
|
||||
</div>
|
||||
|
||||
<div class="minimal-card rounded-lg p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<span class="text-sm font-medium text-subtle">Aujourd'hui</span>
|
||||
<i data-lucide="clock" class="w-4 h-4 text-blue-500"></i>
|
||||
</div>
|
||||
<div class="metric-number text-3xl text-gray-900 mb-1">03</div>
|
||||
<div class="text-xs text-subtle">événements</div>
|
||||
</div>
|
||||
|
||||
<div class="minimal-card rounded-lg p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<span class="text-sm font-medium text-subtle">Demain</span>
|
||||
<i data-lucide="calendar" class="w-4 h-4 text-purple-500"></i>
|
||||
</div>
|
||||
<div class="metric-number text-3xl text-gray-900 mb-1">07</div>
|
||||
<div class="text-xs text-subtle">événements</div>
|
||||
</div>
|
||||
|
||||
<div class="minimal-card rounded-lg p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<span class="text-sm font-medium text-subtle">À venir</span>
|
||||
<i data-lucide="trending-up" class="w-4 h-4 text-orange-500"></i>
|
||||
</div>
|
||||
<div class="metric-number text-3xl text-gray-900 mb-1">15</div>
|
||||
<div class="text-xs text-subtle">cette semaine</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Sections -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
<!-- My Events -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="minimal-card rounded-lg p-6 fade-in">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-semibold">Mes événements</h2>
|
||||
<button class="text-accent text-sm font-medium hover:underline">Voir tout</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Event Row -->
|
||||
<div class="flex items-center space-x-4 py-3 border-b border-gray-100 last:border-b-0">
|
||||
<div class="w-2 h-12 bg-red-400 rounded-full"></div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-medium">Concert Rock Alternative</h3>
|
||||
<span class="mono text-xs bg-green-100 text-green-800 px-2 py-1 rounded">CONFIRMÉ</span>
|
||||
</div>
|
||||
<p class="text-sm text-subtle">Aujourd'hui 21:00 • Salle Pleyel</p>
|
||||
</div>
|
||||
<button class="p-2 hover:bg-gray-100 rounded-lg">
|
||||
<i data-lucide="download" class="w-4 h-4 text-gray-500"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4 py-3 border-b border-gray-100 last:border-b-0">
|
||||
<div class="w-2 h-12 bg-blue-400 rounded-full"></div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-medium">Networking Tech</h3>
|
||||
<span class="mono text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">DEMAIN</span>
|
||||
</div>
|
||||
<p class="text-sm text-subtle">19:00 • WeWork République</p>
|
||||
</div>
|
||||
<button class="p-2 hover:bg-gray-100 rounded-lg">
|
||||
<i data-lucide="map-pin" class="w-4 h-4 text-gray-500"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4 py-3 border-b border-gray-100 last:border-b-0">
|
||||
<div class="w-2 h-12 bg-green-400 rounded-full"></div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-medium">Brunch du Dimanche</h3>
|
||||
<span class="mono text-xs bg-yellow-100 text-yellow-800 px-2 py-1 rounded">DIMANCHE</span>
|
||||
</div>
|
||||
<p class="text-sm text-subtle">11:00 • Café de Flore</p>
|
||||
</div>
|
||||
<button class="p-2 hover:bg-gray-100 rounded-lg">
|
||||
<i data-lucide="calendar" class="w-4 h-4 text-gray-500"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions & Today -->
|
||||
<div class="space-y-6">
|
||||
<!-- Quick Actions -->
|
||||
<div class="minimal-card rounded-lg p-6 fade-in">
|
||||
<h3 class="font-semibold mb-4">Actions rapides</h3>
|
||||
<div class="space-y-3">
|
||||
<button class="w-full flex items-center space-x-3 p-3 rounded-lg hover:bg-gray-50 transition-colors text-left">
|
||||
<div class="p-2 bg-blue-100 rounded-lg">
|
||||
<i data-lucide="plus" class="w-4 h-4 text-blue-600"></i>
|
||||
</div>
|
||||
<span class="font-medium text-sm">Nouvel événement</span>
|
||||
</button>
|
||||
|
||||
<button class="w-full flex items-center space-x-3 p-3 rounded-lg hover:bg-gray-50 transition-colors text-left">
|
||||
<div class="p-2 bg-green-100 rounded-lg">
|
||||
<i data-lucide="search" class="w-4 h-4 text-green-600"></i>
|
||||
</div>
|
||||
<span class="font-medium text-sm">Rechercher</span>
|
||||
</button>
|
||||
|
||||
<button class="w-full flex items-center space-x-3 p-3 rounded-lg hover:bg-gray-50 transition-colors text-left">
|
||||
<div class="p-2 bg-purple-100 rounded-lg">
|
||||
<i data-lucide="heart" class="w-4 h-4 text-purple-600"></i>
|
||||
</div>
|
||||
<span class="font-medium text-sm">Favoris</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Today's Schedule -->
|
||||
<div class="minimal-card rounded-lg p-6 fade-in">
|
||||
<h3 class="font-semibold mb-4">Aujourd'hui</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="mono text-xs bg-gray-100 px-2 py-1 rounded mt-1">14:00</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-medium text-sm">Cours de Cuisine</h4>
|
||||
<p class="text-xs text-subtle">École Ducasse</p>
|
||||
</div>
|
||||
<span class="w-2 h-2 bg-yellow-400 rounded-full mt-2"></span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="mono text-xs bg-gray-100 px-2 py-1 rounded mt-1">20:30</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-medium text-sm">Festival de Cinéma</h4>
|
||||
<p class="text-xs text-subtle">MK2 Bibliothèque</p>
|
||||
</div>
|
||||
<span class="w-2 h-2 bg-red-400 rounded-full mt-2"></span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="mono text-xs bg-gray-100 px-2 py-1 rounded mt-1">22:00</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-medium text-sm">Soirée Jazz</h4>
|
||||
<p class="text-xs text-subtle">Le Sunset</p>
|
||||
</div>
|
||||
<span class="w-2 h-2 bg-blue-400 rounded-full mt-2"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="minimal-card rounded-lg p-6 fade-in">
|
||||
<h3 class="font-semibold mb-4">Statistiques</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-subtle">Total participations</span>
|
||||
<span class="mono font-medium">127</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-subtle">Événements créés</span>
|
||||
<span class="mono font-medium">12</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-subtle">Note moyenne</span>
|
||||
<span class="mono font-medium">4.8/5</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upcoming Events Grid -->
|
||||
<div class="mt-12">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-2xl font-semibold">Événements à venir</h2>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button class="text-sm text-subtle hover:text-gray-900 flex items-center space-x-1">
|
||||
<i data-lucide="filter" class="w-4 h-4"></i>
|
||||
<span>Filtrer</span>
|
||||
</button>
|
||||
<button class="text-sm text-subtle hover:text-gray-900 flex items-center space-x-1">
|
||||
<i data-lucide="grid-3x3" class="w-4 h-4"></i>
|
||||
<span>Vue</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<!-- Event Card -->
|
||||
<div class="minimal-card rounded-lg overflow-hidden fade-in">
|
||||
<div class="aspect-video bg-gradient-to-br from-purple-400 to-purple-600 flex items-center justify-center">
|
||||
<i data-lucide="music" class="w-12 h-12 text-white"></i>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<h3 class="font-semibold">Concert Électro</h3>
|
||||
<span class="mono text-xs bg-gray-100 px-2 py-1 rounded">€45</span>
|
||||
</div>
|
||||
<p class="text-sm text-subtle mb-3">Samedi 21 Sept • Berghain</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-green-600 bg-green-100 px-2 py-1 rounded">12 places restantes</span>
|
||||
<button class="text-accent text-sm font-medium hover:underline">Réserver</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="minimal-card rounded-lg overflow-hidden fade-in">
|
||||
<div class="aspect-video bg-gradient-to-br from-green-400 to-teal-600 flex items-center justify-center">
|
||||
<i data-lucide="leaf" class="w-12 h-12 text-white"></i>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<h3 class="font-semibold">Marché Bio</h3>
|
||||
<span class="mono text-xs bg-green-100 text-green-600 px-2 py-1 rounded">GRATUIT</span>
|
||||
</div>
|
||||
<p class="text-sm text-subtle mb-3">Dimanche 22 Sept • Place des Vosges</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-blue-600 bg-blue-100 px-2 py-1 rounded">Accès libre</span>
|
||||
<button class="text-accent text-sm font-medium hover:underline">Voir détails</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="minimal-card rounded-lg overflow-hidden fade-in">
|
||||
<div class="aspect-video bg-gradient-to-br from-orange-400 to-red-600 flex items-center justify-center">
|
||||
<i data-lucide="book-open" class="w-12 h-12 text-white"></i>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<h3 class="font-semibold">Salon du Livre</h3>
|
||||
<span class="mono text-xs bg-gray-100 px-2 py-1 rounded">€15</span>
|
||||
</div>
|
||||
<p class="text-sm text-subtle mb-3">Lundi 23 Sept • Grand Palais</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-yellow-600 bg-yellow-100 px-2 py-1 rounded">Populaire</span>
|
||||
<button class="text-accent text-sm font-medium hover:underline">Réserver</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load More -->
|
||||
<div class="text-center mt-8">
|
||||
<button class="text-accent font-medium hover:underline">Charger plus d'événements</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
|
||||
// Stagger animations
|
||||
const fadeElements = document.querySelectorAll('.fade-in');
|
||||
fadeElements.forEach((el, index) => {
|
||||
el.style.animationDelay = `${index * 0.1}s`;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,556 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard - Data Visualization Enhanced</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
|
||||
.progress-ring {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.progress-ring__circle {
|
||||
transition: stroke-dashoffset 0.35s;
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.8) 100%);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.gradient-bg {
|
||||
background: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 50%, #06b6d4 100%);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.timeline-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen bg-gray-50">
|
||||
<div class="min-h-screen">
|
||||
<!-- Header -->
|
||||
<div class="gradient-bg px-6 py-8">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold text-white mb-2">Dashboard Analytics</h1>
|
||||
<p class="text-blue-100">Analyse détaillée de vos événements et participations</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<select class="bg-white/20 backdrop-blur-lg border border-white/30 rounded-lg px-4 py-2 text-white text-sm">
|
||||
<option>7 derniers jours</option>
|
||||
<option>30 derniers jours</option>
|
||||
<option>3 derniers mois</option>
|
||||
</select>
|
||||
<button class="bg-white/20 backdrop-blur-lg border border-white/30 rounded-lg px-4 py-2 text-white text-sm font-medium hover:bg-white/30 transition-all">
|
||||
<i data-lucide="download" class="w-4 h-4 inline mr-2"></i>
|
||||
Exporter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI Cards with Progress -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<!-- Participation Rate -->
|
||||
<div class="stat-card rounded-2xl p-6">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-600">Taux de participation</h3>
|
||||
<p class="text-3xl font-bold text-gray-900 mt-2">87%</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<svg class="progress-ring w-16 h-16" viewBox="0 0 40 40">
|
||||
<circle class="progress-ring__circle" stroke="#e5e7eb" stroke-width="3" fill="transparent" r="16" cx="20" cy="20"/>
|
||||
<circle class="progress-ring__circle" stroke="#10b981" stroke-width="3" fill="transparent" r="16" cx="20" cy="20"
|
||||
stroke-dasharray="87 13" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<i data-lucide="trending-up" class="w-6 h-6 text-green-600"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center text-sm">
|
||||
<span class="text-green-600 font-medium">+5%</span>
|
||||
<span class="text-gray-500 ml-1">vs. mois dernier</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Événements créés -->
|
||||
<div class="stat-card rounded-2xl p-6">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-600">Événements créés</h3>
|
||||
<p class="text-3xl font-bold text-gray-900 mt-2">12</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<svg class="progress-ring w-16 h-16" viewBox="0 0 40 40">
|
||||
<circle class="progress-ring__circle" stroke="#e5e7eb" stroke-width="3" fill="transparent" r="16" cx="20" cy="20"/>
|
||||
<circle class="progress-ring__circle" stroke="#3b82f6" stroke-width="3" fill="transparent" r="16" cx="20" cy="20"
|
||||
stroke-dasharray="60 40" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<i data-lucide="plus-circle" class="w-6 h-6 text-blue-600"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center text-sm">
|
||||
<span class="text-blue-600 font-medium">+3</span>
|
||||
<span class="text-gray-500 ml-1">ce mois</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Revenus -->
|
||||
<div class="stat-card rounded-2xl p-6">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-600">Revenus</h3>
|
||||
<p class="text-3xl font-bold text-gray-900 mt-2">€2,340</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<svg class="progress-ring w-16 h-16" viewBox="0 0 40 40">
|
||||
<circle class="progress-ring__circle" stroke="#e5e7eb" stroke-width="3" fill="transparent" r="16" cx="20" cy="20"/>
|
||||
<circle class="progress-ring__circle" stroke="#8b5cf6" stroke-width="3" fill="transparent" r="16" cx="20" cy="20"
|
||||
stroke-dasharray="78 22" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<i data-lucide="euro" class="w-6 h-6 text-purple-600"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center text-sm">
|
||||
<span class="text-purple-600 font-medium">+18%</span>
|
||||
<span class="text-gray-500 ml-1">vs. mois dernier</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Satisfaction -->
|
||||
<div class="stat-card rounded-2xl p-6">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-600">Satisfaction</h3>
|
||||
<p class="text-3xl font-bold text-gray-900 mt-2">4.8</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<svg class="progress-ring w-16 h-16" viewBox="0 0 40 40">
|
||||
<circle class="progress-ring__circle" stroke="#e5e7eb" stroke-width="3" fill="transparent" r="16" cx="20" cy="20"/>
|
||||
<circle class="progress-ring__circle" stroke="#f59e0b" stroke-width="3" fill="transparent" r="16" cx="20" cy="20"
|
||||
stroke-dasharray="96 4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<i data-lucide="star" class="w-6 h-6 text-yellow-500 fill-current"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center text-sm">
|
||||
<span class="text-yellow-600 font-medium">+0.2</span>
|
||||
<span class="text-gray-500 ml-1">vs. mois dernier</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="max-w-7xl mx-auto px-6 py-8">
|
||||
|
||||
<!-- Critical Alert -->
|
||||
<div class="bg-red-50 border border-red-200 rounded-2xl p-6 mb-8">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="p-3 bg-red-100 rounded-xl">
|
||||
<i data-lucide="alert-circle" class="w-6 h-6 text-red-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-red-900 mb-2">Paiements en attente - Action urgente</h3>
|
||||
<p class="text-red-700 mb-4">2 billets expirent dans les 25 prochaines minutes</p>
|
||||
|
||||
<div class="bg-white rounded-xl p-4 border border-red-200">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900">Soirée Jazz au Sunset</h4>
|
||||
<p class="text-sm text-gray-600">2 billets • Tentative 1/3</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-2xl font-bold text-gray-900">€70</p>
|
||||
<div class="w-24 bg-red-200 rounded-full h-2 mt-1">
|
||||
<div class="bg-red-600 h-2 rounded-full transition-all" style="width: 15%"></div>
|
||||
</div>
|
||||
<p class="text-xs text-red-600 mt-1">25min restantes</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="bg-red-600 hover:bg-red-700 text-white px-6 py-3 rounded-xl font-medium transition-colors">
|
||||
Payer maintenant
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts and Analytics -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||
|
||||
<!-- Event Participation Chart -->
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Participation aux événements</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button class="text-sm bg-blue-100 text-blue-700 px-3 py-1 rounded-full">7j</button>
|
||||
<button class="text-sm text-gray-500 px-3 py-1 rounded-full">30j</button>
|
||||
<button class="text-sm text-gray-500 px-3 py-1 rounded-full">3m</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="participationChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Categories Pie Chart -->
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">Catégories d'événements</h3>
|
||||
<div class="chart-container">
|
||||
<canvas id="categoriesChart"></canvas>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-3 h-3 bg-blue-500 rounded-full"></div>
|
||||
<span class="text-sm text-gray-600">Concert (40%)</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||
<span class="text-sm text-gray-600">Cuisine (25%)</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-3 h-3 bg-yellow-500 rounded-full"></div>
|
||||
<span class="text-sm text-gray-600">Tech (20%)</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-3 h-3 bg-purple-500 rounded-full"></div>
|
||||
<span class="text-sm text-gray-600">Art (15%)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeline and Events -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
<!-- Event Timeline -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Timeline des événements</h3>
|
||||
<button class="text-blue-600 text-sm font-medium hover:underline">Voir tout</button>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<div class="absolute left-4 top-0 bottom-0 w-px bg-gray-200"></div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Timeline Item -->
|
||||
<div class="relative pl-10 pb-6">
|
||||
<div class="timeline-item text-green-600">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900">Concert Rock Alternative</h4>
|
||||
<p class="text-sm text-gray-600 mt-1">Aujourd'hui 21:00 • Salle Pleyel</p>
|
||||
<div class="flex items-center space-x-4 mt-2">
|
||||
<div class="flex items-center text-xs">
|
||||
<i data-lucide="users" class="w-3 h-3 mr-1"></i>
|
||||
<span>156 participants</span>
|
||||
</div>
|
||||
<div class="flex items-center text-xs">
|
||||
<i data-lucide="star" class="w-3 h-3 mr-1 fill-current text-yellow-500"></i>
|
||||
<span>4.7/5</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="bg-green-100 text-green-800 px-3 py-1 rounded-full text-xs font-medium">CONFIRMÉ</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative pl-10 pb-6">
|
||||
<div class="timeline-item text-blue-600">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900">Networking Tech</h4>
|
||||
<p class="text-sm text-gray-600 mt-1">Demain 19:00 • WeWork République</p>
|
||||
<div class="flex items-center space-x-4 mt-2">
|
||||
<div class="flex items-center text-xs">
|
||||
<i data-lucide="users" class="w-3 h-3 mr-1"></i>
|
||||
<span>42/50 participants</span>
|
||||
</div>
|
||||
<div class="w-16 bg-gray-200 rounded-full h-1">
|
||||
<div class="bg-blue-600 h-1 rounded-full" style="width: 84%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-xs font-medium">DEMAIN</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative pl-10 pb-6">
|
||||
<div class="timeline-item text-purple-600">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900">Brunch du Dimanche</h4>
|
||||
<p class="text-sm text-gray-600 mt-1">Dimanche 11:00 • Café de Flore</p>
|
||||
<div class="flex items-center space-x-4 mt-2">
|
||||
<div class="flex items-center text-xs">
|
||||
<i data-lucide="users" class="w-3 h-3 mr-1"></i>
|
||||
<span>8/12 participants</span>
|
||||
</div>
|
||||
<div class="w-16 bg-gray-200 rounded-full h-1">
|
||||
<div class="bg-purple-600 h-1 rounded-full" style="width: 67%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="bg-yellow-100 text-yellow-800 px-3 py-1 rounded-full text-xs font-medium">EN COURS</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative pl-10 pb-6">
|
||||
<div class="timeline-item text-gray-400">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900">Cours de Photographie</h4>
|
||||
<p class="text-sm text-gray-600 mt-1">Mercredi 18:00 • Studio Martin</p>
|
||||
<div class="flex items-center space-x-4 mt-2">
|
||||
<div class="flex items-center text-xs">
|
||||
<i data-lucide="calendar" class="w-3 h-3 mr-1"></i>
|
||||
<span>Dans 3 jours</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="bg-gray-100 text-gray-600 px-3 py-1 rounded-full text-xs font-medium">PLANIFIÉ</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Sidebar -->
|
||||
<div class="space-y-6">
|
||||
<!-- Performance Metrics -->
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Performance</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Taux de réussite</span>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-20 bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-green-600 h-2 rounded-full" style="width: 94%"></div>
|
||||
</div>
|
||||
<span class="text-sm font-medium">94%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Engagement</span>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-20 bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-blue-600 h-2 rounded-full" style="width: 78%"></div>
|
||||
</div>
|
||||
<span class="text-sm font-medium">78%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Recommandations</span>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-20 bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-purple-600 h-2 rounded-full" style="width: 89%"></div>
|
||||
</div>
|
||||
<span class="text-sm font-medium">89%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Categories -->
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Top catégories</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<i data-lucide="music" class="w-4 h-4 text-blue-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-sm">Concert</span>
|
||||
<span class="text-sm text-gray-500">40%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-1 mt-1">
|
||||
<div class="bg-blue-600 h-1 rounded-full" style="width: 40%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center">
|
||||
<i data-lucide="utensils" class="w-4 h-4 text-green-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-sm">Cuisine</span>
|
||||
<span class="text-sm text-gray-500">25%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-1 mt-1">
|
||||
<div class="bg-green-600 h-1 rounded-full" style="width: 25%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-yellow-100 rounded-lg flex items-center justify-center">
|
||||
<i data-lucide="laptop" class="w-4 h-4 text-yellow-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-sm">Tech</span>
|
||||
<span class="text-sm text-gray-500">20%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-1 mt-1">
|
||||
<div class="bg-yellow-600 h-1 rounded-full" style="width: 20%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
<i data-lucide="palette" class="w-4 h-4 text-purple-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-sm">Art</span>
|
||||
<span class="text-sm text-gray-500">15%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-1 mt-1">
|
||||
<div class="bg-purple-600 h-1 rounded-full" style="width: 15%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Statistiques rapides</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Événements créés</span>
|
||||
<span class="font-medium">127</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Participants totaux</span>
|
||||
<span class="font-medium">2,456</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Note moyenne</span>
|
||||
<span class="font-medium">4.8/5</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Revenus</span>
|
||||
<span class="font-medium">€12,340</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
|
||||
// Participation Chart
|
||||
const participationCtx = document.getElementById('participationChart').getContext('2d');
|
||||
new Chart(participationCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'],
|
||||
datasets: [{
|
||||
label: 'Participations',
|
||||
data: [12, 19, 8, 15, 24, 18, 22],
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Categories Chart
|
||||
const categoriesCtx = document.getElementById('categoriesChart').getContext('2d');
|
||||
new Chart(categoriesCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Concert', 'Cuisine', 'Tech', 'Art'],
|
||||
datasets: [{
|
||||
data: [40, 25, 20, 15],
|
||||
backgroundColor: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6'],
|
||||
borderWidth: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,521 +0,0 @@
|
||||
/* ========================================
|
||||
Dark Mode UI Framework
|
||||
A beautiful dark mode design system
|
||||
======================================== */
|
||||
|
||||
/* ========================================
|
||||
CSS Variables & Theme
|
||||
======================================== */
|
||||
:root {
|
||||
/* Dark Mode Color Palette */
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
|
||||
/* Spacing & Layout */
|
||||
--radius: 0.625rem;
|
||||
--spacing-xs: 0.25rem;
|
||||
--spacing-sm: 0.5rem;
|
||||
--spacing-md: 0.75rem;
|
||||
--spacing-lg: 1rem;
|
||||
--spacing-xl: 1.5rem;
|
||||
--spacing-2xl: 2rem;
|
||||
--spacing-3xl: 3rem;
|
||||
|
||||
/* Typography */
|
||||
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--font-size-xs: 0.75rem;
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
--font-size-xl: 1.25rem;
|
||||
--font-size-2xl: 1.5rem;
|
||||
--font-size-3xl: 1.875rem;
|
||||
--font-size-4xl: 2.25rem;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Base Styles
|
||||
======================================== */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-family);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Layout Components
|
||||
======================================== */
|
||||
.container {
|
||||
max-width: 64rem;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-2xl) var(--spacing-lg);
|
||||
}
|
||||
|
||||
.container-sm {
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.container-lg {
|
||||
max-width: 80rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
||||
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.grid-cols-auto { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
|
||||
|
||||
.gap-sm { gap: var(--spacing-sm); }
|
||||
.gap-md { gap: var(--spacing-md); }
|
||||
.gap-lg { gap: var(--spacing-lg); }
|
||||
.gap-xl { gap: var(--spacing-xl); }
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Card Components
|
||||
======================================== */
|
||||
.card {
|
||||
background-color: var(--card);
|
||||
color: var(--card-foreground);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: calc(var(--radius) + 4px);
|
||||
padding: var(--spacing-xl);
|
||||
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Button Components
|
||||
======================================== */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-sm);
|
||||
white-space: nowrap;
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: var(--spacing-sm) var(--spacing-lg);
|
||||
min-height: 2.25rem;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: rgba(236, 236, 236, 0.9);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background-color: var(--accent);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background-color: transparent;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background-color: var(--accent);
|
||||
}
|
||||
|
||||
.btn-destructive {
|
||||
background-color: var(--destructive);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-destructive:hover {
|
||||
background-color: rgba(220, 38, 38, 0.9);
|
||||
}
|
||||
|
||||
/* Button Sizes */
|
||||
.btn-sm {
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
font-size: var(--font-size-xs);
|
||||
min-height: 2rem;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: var(--spacing-md) var(--spacing-xl);
|
||||
font-size: var(--font-size-base);
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
padding: var(--spacing-sm);
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Form Components
|
||||
======================================== */
|
||||
.form-input {
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
color: var(--foreground);
|
||||
font-size: var(--font-size-sm);
|
||||
outline: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: var(--ring);
|
||||
box-shadow: 0 0 0 3px rgba(136, 136, 136, 0.5);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Badge Components
|
||||
======================================== */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid;
|
||||
padding: 0.125rem var(--spacing-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Priority Badge Variants */
|
||||
.badge-priority-high {
|
||||
background: rgba(127, 29, 29, 0.3);
|
||||
color: rgb(252, 165, 165);
|
||||
border: 1px solid rgba(153, 27, 27, 0.5);
|
||||
}
|
||||
|
||||
.badge-priority-medium {
|
||||
background: rgba(120, 53, 15, 0.3);
|
||||
color: rgb(252, 211, 77);
|
||||
border: 1px solid rgba(146, 64, 14, 0.5);
|
||||
}
|
||||
|
||||
.badge-priority-low {
|
||||
background: rgba(20, 83, 45, 0.3);
|
||||
color: rgb(134, 239, 172);
|
||||
border: 1px solid rgba(22, 101, 52, 0.5);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Tab Components
|
||||
======================================== */
|
||||
.tab-list {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
background-color: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: var(--foreground);
|
||||
text-transform: capitalize;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
background-color: #f8f9fa !important;
|
||||
color: #1a1a1a !important;
|
||||
border-color: #f8f9fa !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-button.active:hover {
|
||||
background-color: #e9ecef !important;
|
||||
border-color: #e9ecef !important;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Typography
|
||||
======================================== */
|
||||
.text-xs { font-size: var(--font-size-xs); }
|
||||
.text-sm { font-size: var(--font-size-sm); }
|
||||
.text-base { font-size: var(--font-size-base); }
|
||||
.text-lg { font-size: var(--font-size-lg); }
|
||||
.text-xl { font-size: var(--font-size-xl); }
|
||||
.text-2xl { font-size: var(--font-size-2xl); }
|
||||
.text-3xl { font-size: var(--font-size-3xl); }
|
||||
.text-4xl { font-size: var(--font-size-4xl); }
|
||||
|
||||
.font-normal { font-weight: 400; }
|
||||
.font-medium { font-weight: 500; }
|
||||
.font-semibold { font-weight: 600; }
|
||||
.font-bold { font-weight: 700; }
|
||||
|
||||
.text-primary { color: var(--primary); }
|
||||
.text-muted { color: var(--muted-foreground); }
|
||||
.text-destructive { color: var(--destructive); }
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(to right, var(--primary), rgba(236, 236, 236, 0.6));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Icon System
|
||||
======================================== */
|
||||
.icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
fill: currentColor;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon-sm { width: 0.875rem; height: 0.875rem; }
|
||||
.icon-lg { width: 1.25rem; height: 1.25rem; }
|
||||
.icon-xl { width: 1.5rem; height: 1.5rem; }
|
||||
.icon-2xl { width: 2rem; height: 2rem; }
|
||||
|
||||
/* ========================================
|
||||
Interactive Components
|
||||
======================================== */
|
||||
.checkbox {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.checkbox:hover {
|
||||
border-color: var(--ring);
|
||||
}
|
||||
|
||||
.checkbox.checked {
|
||||
background-color: rgb(22, 163, 74);
|
||||
border-color: rgb(22, 163, 74);
|
||||
}
|
||||
|
||||
.checkbox.checked::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
List Components
|
||||
======================================== */
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-lg);
|
||||
padding: var(--spacing-lg);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.list-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.025);
|
||||
}
|
||||
|
||||
.list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.list-item.completed {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Empty State Component
|
||||
======================================== */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: var(--spacing-3xl) var(--spacing-lg);
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.empty-state .icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin: 0 auto var(--spacing-lg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Utility Classes
|
||||
======================================== */
|
||||
.hidden { display: none; }
|
||||
.block { display: block; }
|
||||
.flex { display: flex; }
|
||||
.inline-flex { display: inline-flex; }
|
||||
|
||||
.w-full { width: 100%; }
|
||||
.h-full { height: 100%; }
|
||||
.min-h-screen { min-height: 100vh; }
|
||||
|
||||
.opacity-50 { opacity: 0.5; }
|
||||
.opacity-60 { opacity: 0.6; }
|
||||
.opacity-75 { opacity: 0.75; }
|
||||
|
||||
.transition-all { transition: all 0.2s ease; }
|
||||
.transition-colors { transition: color 0.2s ease, background-color 0.2s ease; }
|
||||
.transition-opacity { transition: opacity 0.2s ease; }
|
||||
|
||||
/* ========================================
|
||||
Responsive Design
|
||||
======================================== */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.grid-cols-auto {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.flex-col-mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.text-center-mobile {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gap-sm-mobile { gap: var(--spacing-sm); }
|
||||
|
||||
.hidden-mobile { display: none; }
|
||||
.block-mobile { display: block; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.text-2xl { font-size: var(--font-size-xl); }
|
||||
.text-3xl { font-size: var(--font-size-2xl); }
|
||||
.text-4xl { font-size: var(--font-size-3xl); }
|
||||
|
||||
.container {
|
||||
padding: var(--spacing-lg) var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Animation Utilities
|
||||
======================================== */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Focus & Accessibility
|
||||
======================================== */
|
||||
.focus-visible:focus-visible {
|
||||
outline: 2px solid var(--ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
:root {
|
||||
--background: oklch(0.9961 0.0039 106.7952);
|
||||
--foreground: oklch(0.0902 0.0203 286.0532);
|
||||
--card: oklch(0.9961 0.0039 106.7952);
|
||||
--card-foreground: oklch(0.0902 0.0203 286.0532);
|
||||
--popover: oklch(0.9961 0.0039 106.7952);
|
||||
--popover-foreground: oklch(0.0902 0.0203 286.0532);
|
||||
--primary: oklch(0.4902 0.2314 320.7094);
|
||||
--primary-foreground: oklch(0.9961 0.0039 106.7952);
|
||||
--secondary: oklch(0.6471 0.1686 342.5570);
|
||||
--secondary-foreground: oklch(0.0902 0.0203 286.0532);
|
||||
--muted: oklch(0.9412 0.0196 106.7952);
|
||||
--muted-foreground: oklch(0.4706 0.0157 286.0532);
|
||||
--accent: oklch(0.7255 0.1451 51.2345);
|
||||
--accent-foreground: oklch(0.0902 0.0203 286.0532);
|
||||
--destructive: oklch(0.5765 0.2314 27.3319);
|
||||
--destructive-foreground: oklch(0.9961 0.0039 106.7952);
|
||||
--border: oklch(0.8824 0.0157 106.7952);
|
||||
--input: oklch(0.8824 0.0157 106.7952);
|
||||
--ring: oklch(0.4902 0.2314 320.7094);
|
||||
--chart-1: oklch(0.4902 0.2314 320.7094);
|
||||
--chart-2: oklch(0.6471 0.1686 342.5570);
|
||||
--chart-3: oklch(0.7255 0.1451 51.2345);
|
||||
--chart-4: oklch(0.5490 0.2157 142.4953);
|
||||
--chart-5: oklch(0.6157 0.2275 328.3634);
|
||||
--sidebar: oklch(0.9412 0.0196 106.7952);
|
||||
--sidebar-foreground: oklch(0.0902 0.0203 286.0532);
|
||||
--sidebar-primary: oklch(0.4902 0.2314 320.7094);
|
||||
--sidebar-primary-foreground: oklch(0.9961 0.0039 106.7952);
|
||||
--sidebar-accent: oklch(0.6471 0.1686 342.5570);
|
||||
--sidebar-accent-foreground: oklch(0.0902 0.0203 286.0532);
|
||||
--sidebar-border: oklch(0.8824 0.0157 106.7952);
|
||||
--sidebar-ring: oklch(0.4902 0.2314 320.7094);
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
--font-serif: 'Playfair Display', serif;
|
||||
--font-mono: 'Fira Code', monospace;
|
||||
--radius: 1rem;
|
||||
--shadow-2xs: 0 1px 2px 0px hsl(320 70% 20% / 0.08);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(320 70% 20% / 0.10);
|
||||
--shadow-sm: 0 2px 4px 0px hsl(320 70% 20% / 0.10), 0 1px 2px -1px hsl(320 70% 20% / 0.06);
|
||||
--shadow: 0 4px 6px 0px hsl(320 70% 20% / 0.12), 0 2px 4px -1px hsl(320 70% 20% / 0.08);
|
||||
--shadow-md: 0 6px 8px 0px hsl(320 70% 20% / 0.15), 0 4px 6px -1px hsl(320 70% 20% / 0.10);
|
||||
--shadow-lg: 0 10px 15px 0px hsl(320 70% 20% / 0.20), 0 6px 8px -1px hsl(320 70% 20% / 0.15);
|
||||
--shadow-xl: 0 20px 25px 0px hsl(320 70% 20% / 0.25), 0 10px 15px -1px hsl(320 70% 20% / 0.20);
|
||||
--shadow-2xl: 0 25px 50px 0px hsl(320 70% 20% / 0.30);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
@@ -1,538 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Fête de l'Humanité 2025 - Billets</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Playfair+Display:wght@400;500;600;700&family=Fira+Code:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="festival_theme.css">
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans) !important;
|
||||
background: var(--background) !important;
|
||||
color: var(--foreground) !important;
|
||||
line-height: 1.6 !important;
|
||||
}
|
||||
|
||||
.festival-gradient {
|
||||
background: linear-gradient(135deg,
|
||||
oklch(0.4902 0.2314 320.7094) 0%,
|
||||
oklch(0.6471 0.1686 342.5570) 50%,
|
||||
oklch(0.7255 0.1451 51.2345) 100%) !important;
|
||||
}
|
||||
|
||||
.ticket-card {
|
||||
background: var(--card) !important;
|
||||
border: 2px solid var(--border) !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
}
|
||||
|
||||
.ticket-card:hover {
|
||||
transform: translateY(-4px) !important;
|
||||
box-shadow: var(--shadow-lg) !important;
|
||||
border-color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.ticket-card.selected {
|
||||
border-color: var(--primary) !important;
|
||||
background: linear-gradient(135deg, var(--card), oklch(0.4902 0.2314 320.7094 / 0.05)) !important;
|
||||
box-shadow: var(--shadow-lg) !important;
|
||||
}
|
||||
|
||||
.quantity-control {
|
||||
background: var(--muted) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: var(--radius) !important;
|
||||
transition: all 0.2s ease !important;
|
||||
}
|
||||
|
||||
.quantity-control:hover {
|
||||
background: var(--accent) !important;
|
||||
transform: scale(1.05) !important;
|
||||
}
|
||||
|
||||
.cart-summary {
|
||||
background: linear-gradient(135deg,
|
||||
var(--card),
|
||||
oklch(0.4902 0.2314 320.7094 / 0.03)) !important;
|
||||
border: 2px solid var(--primary) !important;
|
||||
border-radius: var(--radius-xl) !important;
|
||||
box-shadow: var(--shadow-md) !important;
|
||||
}
|
||||
|
||||
.checkout-button {
|
||||
background: var(--primary) !important;
|
||||
color: var(--primary-foreground) !important;
|
||||
border: none !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
font-weight: 600 !important;
|
||||
transition: all 0.3s ease !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
}
|
||||
|
||||
.checkout-button:hover:not(:disabled) {
|
||||
background: oklch(0.4302 0.2314 320.7094) !important;
|
||||
transform: translateY(-2px) !important;
|
||||
box-shadow: var(--shadow-lg) !important;
|
||||
}
|
||||
|
||||
.checkout-button:disabled {
|
||||
background: var(--muted) !important;
|
||||
color: var(--muted-foreground) !important;
|
||||
cursor: not-allowed !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
.festival-info {
|
||||
background: linear-gradient(45deg,
|
||||
oklch(0.7255 0.1451 51.2345 / 0.1),
|
||||
oklch(0.6471 0.1686 342.5570 / 0.1)) !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
border: 1px solid var(--accent) !important;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
background: linear-gradient(135deg,
|
||||
oklch(0.4902 0.2314 320.7094 / 0.9) 0%,
|
||||
oklch(0.6471 0.1686 342.5570 / 0.9) 50%,
|
||||
oklch(0.7255 0.1451 51.2345 / 0.9) 100%),
|
||||
url('https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=1200&h=600&fit=crop') !important;
|
||||
background-size: cover !important;
|
||||
background-position: center !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.animate-bounce-slow {
|
||||
animation: bounce 2s infinite !important;
|
||||
}
|
||||
|
||||
.animate-pulse-slow {
|
||||
animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
|
||||
}
|
||||
|
||||
.ripple-effect {
|
||||
position: relative !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.ripple-effect::after {
|
||||
content: '' !important;
|
||||
position: absolute !important;
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
border-radius: 50% !important;
|
||||
background: rgba(255, 255, 255, 0.3) !important;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
transition: width 0.4s, height 0.4s !important;
|
||||
}
|
||||
|
||||
.ripple-effect:hover::after {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section h-96 flex items-center justify-center relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-black bg-opacity-40"></div>
|
||||
<div class="relative z-10 text-center max-w-4xl mx-auto px-4">
|
||||
<h1 class="text-5xl md:text-6xl font-bold mb-4 font-serif animate-pulse-slow">Fête de l'Humanité 2025</h1>
|
||||
<p class="text-xl md:text-2xl mb-2 opacity-90">14-16 Septembre • La Courneuve</p>
|
||||
<p class="text-lg opacity-80 max-w-2xl mx-auto">Trois jours de musique, débats, culture et solidarité au cœur du plus grand festival populaire de France</p>
|
||||
<div class="flex justify-center items-center mt-6 space-x-6">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="calendar" class="w-5 h-5 mr-2"></i>
|
||||
<span>3 jours</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="music" class="w-5 h-5 mr-2"></i>
|
||||
<span>100+ concerts</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="users" class="w-5 h-5 mr-2"></i>
|
||||
<span>500k visiteurs</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute bottom-4 left-1/2 transform -translate-x-1/2 animate-bounce-slow">
|
||||
<i data-lucide="chevron-down" class="w-8 h-8 text-white opacity-70"></i>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<!-- Ticket Selection Hub -->
|
||||
<div class="mb-12">
|
||||
<div class="text-center mb-10">
|
||||
<h2 class="text-4xl font-bold text-gray-900 mb-4 font-serif">Choisissez vos billets</h2>
|
||||
<p class="text-xl text-gray-600 max-w-2xl mx-auto">Découvrez nos différentes formules pour profiter pleinement du festival</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Left Column: Tickets -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 mb-8">
|
||||
<!-- Pass 3 Jours -->
|
||||
<div class="ticket-card p-6 cursor-pointer ripple-effect" onclick="selectTicket('pass3j', 45, 'Pass 3 jours')">
|
||||
<div class="text-center">
|
||||
<div class="festival-gradient w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i data-lucide="star" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 mb-2">Pass 3 Jours</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Accès complet au festival</p>
|
||||
<div class="text-3xl font-bold text-primary mb-4">45€</div>
|
||||
<div class="text-sm text-green-600 font-medium mb-4">✓ Disponible</div>
|
||||
|
||||
<div class="flex items-center justify-center space-x-3">
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('pass3j', -1)">
|
||||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<span class="w-8 text-center font-medium" id="pass3j-qty">0</span>
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('pass3j', 1)">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Samedi 14 -->
|
||||
<div class="ticket-card p-6 cursor-pointer ripple-effect" onclick="selectTicket('samedi', 18, 'Samedi 14 Sept')">
|
||||
<div class="text-center">
|
||||
<div class="bg-gradient-to-br from-purple-500 to-pink-500 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i data-lucide="calendar-days" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 mb-2">Samedi 14</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Journée complète</p>
|
||||
<div class="text-3xl font-bold text-primary mb-4">18€</div>
|
||||
<div class="text-sm text-green-600 font-medium mb-4">✓ Disponible</div>
|
||||
|
||||
<div class="flex items-center justify-center space-x-3">
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('samedi', -1)">
|
||||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<span class="w-8 text-center font-medium" id="samedi-qty">0</span>
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('samedi', 1)">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dimanche 15 -->
|
||||
<div class="ticket-card p-6 cursor-pointer ripple-effect" onclick="selectTicket('dimanche', 18, 'Dimanche 15 Sept')">
|
||||
<div class="text-center">
|
||||
<div class="bg-gradient-to-br from-orange-500 to-red-500 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i data-lucide="sun" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 mb-2">Dimanche 15</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Journée complète</p>
|
||||
<div class="text-3xl font-bold text-primary mb-4">18€</div>
|
||||
<div class="text-sm text-green-600 font-medium mb-4">✓ Disponible</div>
|
||||
|
||||
<div class="flex items-center justify-center space-x-3">
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('dimanche', -1)">
|
||||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<span class="w-8 text-center font-medium" id="dimanche-qty">0</span>
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('dimanche', 1)">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lundi 16 -->
|
||||
<div class="ticket-card p-6 cursor-pointer ripple-effect" onclick="selectTicket('lundi', 18, 'Lundi 16 Sept')">
|
||||
<div class="text-center">
|
||||
<div class="bg-gradient-to-br from-green-500 to-blue-500 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i data-lucide="moon" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 mb-2">Lundi 16</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Journée complète</p>
|
||||
<div class="text-3xl font-bold text-primary mb-4">18€</div>
|
||||
<div class="text-sm text-green-600 font-medium mb-4">✓ Disponible</div>
|
||||
|
||||
<div class="flex items-center justify-center space-x-3">
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('lundi', -1)">
|
||||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<span class="w-8 text-center font-medium" id="lundi-qty">0</span>
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('lundi', 1)">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tarif Réduit -->
|
||||
<div class="ticket-card p-6 cursor-pointer ripple-effect" onclick="selectTicket('reduit', 12, 'Tarif Réduit')">
|
||||
<div class="text-center">
|
||||
<div class="bg-gradient-to-br from-yellow-500 to-orange-500 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i data-lucide="percent" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 mb-2">Tarif Réduit</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Étudiants, -26 ans, RSA</p>
|
||||
<div class="text-3xl font-bold text-primary mb-4">12€</div>
|
||||
<div class="text-sm text-green-600 font-medium mb-4">✓ Disponible</div>
|
||||
|
||||
<div class="flex items-center justify-center space-x-3">
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('reduit', -1)">
|
||||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<span class="w-8 text-center font-medium" id="reduit-qty">0</span>
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('reduit', 1)">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gratuit -12 ans -->
|
||||
<div class="ticket-card p-6 cursor-pointer ripple-effect" onclick="selectTicket('gratuit', 0, 'Gratuit -12 ans')">
|
||||
<div class="text-center">
|
||||
<div class="bg-gradient-to-br from-green-600 to-emerald-600 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i data-lucide="gift" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 mb-2">Gratuit</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Enfants -12 ans</p>
|
||||
<div class="text-3xl font-bold text-green-600 mb-4">Gratuit</div>
|
||||
<div class="text-sm text-green-600 font-medium mb-4">✓ Disponible</div>
|
||||
|
||||
<div class="flex items-center justify-center space-x-3">
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('gratuit', -1)">
|
||||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<span class="w-8 text-center font-medium" id="gratuit-qty">0</span>
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('gratuit', 1)">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Cart & Info -->
|
||||
<div class="lg:col-span-1">
|
||||
<!-- Cart Summary -->
|
||||
<div class="cart-summary p-6 mb-8 sticky top-4">
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-6 text-center">Récapitulatif</h3>
|
||||
|
||||
<div id="cart-items" class="space-y-3 mb-6 min-h-[100px]">
|
||||
<div class="text-center text-gray-500 py-8">
|
||||
<i data-lucide="shopping-cart" class="w-12 h-12 mx-auto mb-4 opacity-50"></i>
|
||||
<p>Votre panier est vide</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 pt-4 space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Total billets:</span>
|
||||
<span class="font-medium" id="total-quantity">0</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Sous-total:</span>
|
||||
<span class="font-medium" id="subtotal">€0.00</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Frais de service:</span>
|
||||
<span class="font-medium" id="service-fee">€0.00</span>
|
||||
</div>
|
||||
<div class="border-t border-gray-300 pt-2 mt-4">
|
||||
<div class="flex justify-between text-lg font-bold">
|
||||
<span>TOTAL:</span>
|
||||
<span class="text-primary" id="total-amount">€0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="checkout-btn" class="checkout-button w-full py-4 px-6 text-lg font-semibold mt-6 disabled" disabled>
|
||||
<i data-lucide="credit-card" class="w-5 h-5 inline-block mr-2"></i>
|
||||
Finaliser la commande
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Festival Info -->
|
||||
<div class="festival-info p-6">
|
||||
<h4 class="text-xl font-bold text-gray-900 mb-4 text-center">🎪 Festival Highlights</h4>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="music" class="w-5 h-5 mr-3 text-purple-600"></i>
|
||||
<span>100+ concerts et spectacles</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="mic" class="w-5 h-5 mr-3 text-purple-600"></i>
|
||||
<span>Débats et conférences</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="utensils" class="w-5 h-5 mr-3 text-purple-600"></i>
|
||||
<span>Village gastronomique</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="heart" class="w-5 h-5 mr-3 text-purple-600"></i>
|
||||
<span>Village solidaire</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="gamepad-2" class="w-5 h-5 mr-3 text-purple-600"></i>
|
||||
<span>Animations jeunesse</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="train" class="w-5 h-5 mr-3 text-purple-600"></i>
|
||||
<span>Accès RER B La Courneuve</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize Lucide icons
|
||||
lucide.createIcons();
|
||||
|
||||
// Cart state
|
||||
let cart = {};
|
||||
const serviceFeeRate = 0.05; // 5% service fee
|
||||
|
||||
function selectTicket(id, price, name) {
|
||||
// Visual selection effect
|
||||
const cards = document.querySelectorAll('.ticket-card');
|
||||
cards.forEach(card => card.classList.remove('selected'));
|
||||
event.currentTarget.classList.add('selected');
|
||||
|
||||
// Auto-add one ticket if none selected
|
||||
if (!cart[id] || cart[id].quantity === 0) {
|
||||
changeQuantity(id, 1, price, name);
|
||||
}
|
||||
}
|
||||
|
||||
function changeQuantity(id, delta, price, name) {
|
||||
if (!cart[id]) {
|
||||
cart[id] = { quantity: 0, price: price || 0, name: name || '' };
|
||||
}
|
||||
|
||||
// Get price and name from ticket data if not provided
|
||||
if (!price) {
|
||||
const ticketPrices = {
|
||||
'pass3j': { price: 45, name: 'Pass 3 jours' },
|
||||
'samedi': { price: 18, name: 'Samedi 14 Sept' },
|
||||
'dimanche': { price: 18, name: 'Dimanche 15 Sept' },
|
||||
'lundi': { price: 18, name: 'Lundi 16 Sept' },
|
||||
'reduit': { price: 12, name: 'Tarif Réduit' },
|
||||
'gratuit': { price: 0, name: 'Gratuit -12 ans' }
|
||||
};
|
||||
price = ticketPrices[id].price;
|
||||
name = ticketPrices[id].name;
|
||||
cart[id].price = price;
|
||||
cart[id].name = name;
|
||||
}
|
||||
|
||||
cart[id].quantity = Math.max(0, cart[id].quantity + delta);
|
||||
|
||||
// Update quantity display
|
||||
document.getElementById(id + '-qty').textContent = cart[id].quantity;
|
||||
|
||||
// Remove from cart if quantity is 0
|
||||
if (cart[id].quantity === 0) {
|
||||
delete cart[id];
|
||||
}
|
||||
|
||||
updateCartSummary();
|
||||
}
|
||||
|
||||
function updateCartSummary() {
|
||||
const cartItemsContainer = document.getElementById('cart-items');
|
||||
const totalQuantityEl = document.getElementById('total-quantity');
|
||||
const subtotalEl = document.getElementById('subtotal');
|
||||
const serviceFeeEl = document.getElementById('service-fee');
|
||||
const totalAmountEl = document.getElementById('total-amount');
|
||||
const checkoutBtn = document.getElementById('checkout-btn');
|
||||
|
||||
let totalQuantity = 0;
|
||||
let subtotal = 0;
|
||||
let cartItemsHtml = '';
|
||||
|
||||
// Check if cart is empty
|
||||
const hasItems = Object.keys(cart).some(id => cart[id].quantity > 0);
|
||||
|
||||
if (!hasItems) {
|
||||
cartItemsHtml = `
|
||||
<div class="text-center text-gray-500 py-8">
|
||||
<i data-lucide="shopping-cart" class="w-12 h-12 mx-auto mb-4 opacity-50"></i>
|
||||
<p>Votre panier est vide</p>
|
||||
</div>
|
||||
`;
|
||||
checkoutBtn.disabled = true;
|
||||
checkoutBtn.classList.add('disabled');
|
||||
} else {
|
||||
// Build cart items
|
||||
Object.keys(cart).forEach(id => {
|
||||
if (cart[id].quantity > 0) {
|
||||
totalQuantity += cart[id].quantity;
|
||||
subtotal += cart[id].quantity * cart[id].price;
|
||||
|
||||
cartItemsHtml += `
|
||||
<div class="flex justify-between items-center py-2 border-b border-gray-100 last:border-b-0">
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-sm">${cart[id].name}</div>
|
||||
<div class="text-xs text-gray-500">${cart[id].quantity} × €${cart[id].price.toFixed(2)}</div>
|
||||
</div>
|
||||
<div class="font-medium text-sm">€${(cart[id].quantity * cart[id].price).toFixed(2)}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
checkoutBtn.disabled = false;
|
||||
checkoutBtn.classList.remove('disabled');
|
||||
}
|
||||
|
||||
const serviceFee = subtotal * serviceFeeRate;
|
||||
const totalAmount = subtotal + serviceFee;
|
||||
|
||||
cartItemsContainer.innerHTML = cartItemsHtml;
|
||||
totalQuantityEl.textContent = totalQuantity;
|
||||
subtotalEl.textContent = `€${subtotal.toFixed(2)}`;
|
||||
serviceFeeEl.textContent = `€${serviceFee.toFixed(2)}`;
|
||||
totalAmountEl.textContent = `€${totalAmount.toFixed(2)}`;
|
||||
|
||||
// Recreate icons for newly added elements
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// Checkout button click handler
|
||||
document.getElementById('checkout-btn').addEventListener('click', function() {
|
||||
if (this.disabled) return;
|
||||
|
||||
// Simulate checkout process
|
||||
this.innerHTML = '<i data-lucide="loader-2" class="w-5 h-5 inline-block mr-2 animate-spin"></i>Traitement...';
|
||||
this.disabled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
alert('Redirection vers le paiement sécurisé...');
|
||||
this.innerHTML = '<i data-lucide="credit-card" class="w-5 h-5 inline-block mr-2"></i>Finaliser la commande';
|
||||
this.disabled = Object.keys(cart).length === 0;
|
||||
lucide.createIcons();
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// Initial setup
|
||||
updateCartSummary();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,480 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Connexion - Quantic Telecom</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="quantic_telecom_theme.css">
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans) !important;
|
||||
background: var(--gradient-background) !important;
|
||||
min-height: 100vh !important;
|
||||
position: relative !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
/* Background grid pattern */
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
linear-gradient(var(--grid-color) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
opacity: 0.3;
|
||||
z-index: 0;
|
||||
animation: gridShift 20s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes gridShift {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(50px, 50px); }
|
||||
}
|
||||
|
||||
/* Page entrance animation */
|
||||
.page-container {
|
||||
animation: pageLoad 800ms ease-out forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(40px);
|
||||
}
|
||||
|
||||
@keyframes pageLoad {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Logo animation */
|
||||
.logo-container {
|
||||
animation: logoFade 1200ms ease-out 200ms forwards;
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
@keyframes logoFade {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Card slide animation */
|
||||
.login-card {
|
||||
background: var(--glass-bg) !important;
|
||||
backdrop-filter: var(--glass-backdrop) !important;
|
||||
border: 1px solid var(--glass-border) !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
box-shadow: var(--shadow-xl) !important;
|
||||
animation: cardSlide 600ms cubic-bezier(0.4, 0, 0.2, 1) 400ms forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 300ms ease-out;
|
||||
}
|
||||
|
||||
.login-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-2xl);
|
||||
}
|
||||
|
||||
@keyframes cardSlide {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Input field styling */
|
||||
.input-group {
|
||||
position: relative;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
padding: 1rem 3rem 1rem 1rem;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--input);
|
||||
color: var(--foreground);
|
||||
font-size: 1rem;
|
||||
transition: all 200ms ease-out;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px var(--ring);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.input-field:focus + .floating-label {
|
||||
transform: translateY(-10px) scale(0.75);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.floating-label {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: var(--input);
|
||||
padding: 0 0.5rem;
|
||||
color: var(--muted-foreground);
|
||||
pointer-events: none;
|
||||
transition: all 200ms ease-out;
|
||||
}
|
||||
|
||||
.input-field:not(:placeholder-shown) + .floating-label {
|
||||
transform: translateY(-10px) scale(0.75);
|
||||
}
|
||||
|
||||
/* Password toggle */
|
||||
.password-toggle {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted-foreground);
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease-out;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.password-toggle:hover {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) rotate(90deg);
|
||||
}
|
||||
|
||||
/* Button styling */
|
||||
.login-button {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
background: var(--gradient-primary) !important;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--primary-foreground);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 200ms ease-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.login-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Ripple effect */
|
||||
.login-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transition: width 400ms ease-out, height 400ms ease-out;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.login-button:active::before {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
/* Checkbox styling */
|
||||
.custom-checkbox {
|
||||
appearance: none;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--input);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 200ms ease-out;
|
||||
}
|
||||
|
||||
.custom-checkbox:checked {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
animation: checkboxTick 250ms cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
.custom-checkbox:checked::before {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: var(--primary-foreground);
|
||||
font-size: 0.875rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@keyframes checkboxTick {
|
||||
0% { transform: scale(0); }
|
||||
50% { transform: scale(1.2); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Link styling */
|
||||
.forgot-link {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
transition: all 200ms ease-out;
|
||||
}
|
||||
|
||||
.forgot-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
transition: width 200ms ease-out;
|
||||
}
|
||||
|
||||
.forgot-link:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Validation states */
|
||||
.input-error {
|
||||
border-color: var(--destructive) !important;
|
||||
animation: errorShake 300ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes errorShake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-8px); }
|
||||
75% { transform: translateX(8px); }
|
||||
}
|
||||
|
||||
.input-success {
|
||||
border-color: var(--success) !important;
|
||||
animation: successPulse 500ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes successPulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); opacity: 0.8; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Loading states */
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, var(--muted) 25%, var(--accent) 50%, var(--muted) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes skeleton {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 640px) {
|
||||
.login-card {
|
||||
margin: 1rem;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-container relative z-10 flex items-center justify-center min-h-screen p-4">
|
||||
<div class="w-full max-w-md">
|
||||
<!-- Logo and Header -->
|
||||
<div class="logo-container text-center mb-8">
|
||||
<div class="mb-4">
|
||||
<div class="w-16 h-16 mx-auto bg-gradient-to-br from-blue-600 to-blue-800 rounded-xl flex items-center justify-center">
|
||||
<i data-lucide="wifi" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-2">Quantic Telecom</h1>
|
||||
<p class="text-gray-600 text-sm">Connexion Espace Client</p>
|
||||
<p class="text-gray-500 text-xs mt-1">Votre espace client sécurisé</p>
|
||||
</div>
|
||||
|
||||
<!-- Login Card -->
|
||||
<div class="login-card p-8">
|
||||
<form class="space-y-6">
|
||||
<!-- Email Field -->
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="email"
|
||||
class="input-field"
|
||||
placeholder=" "
|
||||
required
|
||||
id="email"
|
||||
>
|
||||
<label class="floating-label" for="email">Adresse e-mail</label>
|
||||
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<i data-lucide="mail" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="password"
|
||||
class="input-field"
|
||||
placeholder=" "
|
||||
required
|
||||
id="password"
|
||||
>
|
||||
<label class="floating-label" for="password">Mot de passe</label>
|
||||
<button type="button" class="password-toggle" onclick="togglePassword()">
|
||||
<i data-lucide="eye" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Remember Me -->
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center space-x-3 cursor-pointer">
|
||||
<input type="checkbox" class="custom-checkbox" id="remember">
|
||||
<span class="text-sm text-gray-700">Se souvenir de moi</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Login Button -->
|
||||
<button type="submit" class="login-button">
|
||||
<span class="relative z-10">SE CONNECTER</span>
|
||||
</button>
|
||||
|
||||
<!-- Forgot Password -->
|
||||
<div class="text-center">
|
||||
<a href="#" class="forgot-link text-sm">Mot de passe oublié ?</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Support Footer -->
|
||||
<div class="text-center mt-8 space-y-2">
|
||||
<p class="text-xs text-gray-500">
|
||||
Besoin d'aide ?
|
||||
<a href="#" class="text-blue-600 hover:text-blue-800 transition-colors">Support technique</a>
|
||||
</p>
|
||||
<p class="text-xs text-gray-400">© 2024 Quantic Telecom - Tous droits réservés</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize Lucide icons
|
||||
lucide.createIcons();
|
||||
|
||||
// Password toggle functionality
|
||||
function togglePassword() {
|
||||
const passwordField = document.getElementById('password');
|
||||
const toggleIcon = document.querySelector('.password-toggle i');
|
||||
|
||||
if (passwordField.type === 'password') {
|
||||
passwordField.type = 'text';
|
||||
toggleIcon.setAttribute('data-lucide', 'eye-off');
|
||||
} else {
|
||||
passwordField.type = 'password';
|
||||
toggleIcon.setAttribute('data-lucide', 'eye');
|
||||
}
|
||||
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// Form validation
|
||||
const form = document.querySelector('form');
|
||||
const emailField = document.getElementById('email');
|
||||
const passwordField = document.getElementById('password');
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Reset validation states
|
||||
emailField.classList.remove('input-error', 'input-success');
|
||||
passwordField.classList.remove('input-error', 'input-success');
|
||||
|
||||
let isValid = true;
|
||||
|
||||
// Email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(emailField.value)) {
|
||||
emailField.classList.add('input-error');
|
||||
isValid = false;
|
||||
} else {
|
||||
emailField.classList.add('input-success');
|
||||
}
|
||||
|
||||
// Password validation
|
||||
if (passwordField.value.length < 6) {
|
||||
passwordField.classList.add('input-error');
|
||||
isValid = false;
|
||||
} else {
|
||||
passwordField.classList.add('input-success');
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
// Simulate login process
|
||||
const button = document.querySelector('.login-button');
|
||||
button.innerHTML = '<div class="flex items-center justify-center"><div class="animate-spin w-5 h-5 border-2 border-white border-t-transparent rounded-full mr-2"></div>Connexion...</div>';
|
||||
|
||||
setTimeout(() => {
|
||||
alert('Connexion réussie ! (Demo)');
|
||||
button.innerHTML = '<span class="relative z-10">SE CONNECTER</span>';
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
// Real-time input validation
|
||||
emailField.addEventListener('input', function() {
|
||||
this.classList.remove('input-error', 'input-success');
|
||||
});
|
||||
|
||||
passwordField.addEventListener('input', function() {
|
||||
this.classList.remove('input-error', 'input-success');
|
||||
});
|
||||
|
||||
// Add floating label behavior for better UX
|
||||
document.querySelectorAll('.input-field').forEach(input => {
|
||||
input.addEventListener('focus', function() {
|
||||
this.nextElementSibling.classList.add('focused');
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function() {
|
||||
if (!this.value) {
|
||||
this.nextElementSibling.classList.remove('focused');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,71 +0,0 @@
|
||||
:root {
|
||||
/* Quantic Telecom Brand Colors */
|
||||
--background: oklch(0.9800 0.0050 240);
|
||||
--foreground: oklch(0.1500 0.0100 240);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0.1500 0.0100 240);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0.1500 0.0100 240);
|
||||
|
||||
/* Primary - Telecom Blue */
|
||||
--primary: oklch(0.4800 0.2000 240);
|
||||
--primary-foreground: oklch(0.9800 0.0050 240);
|
||||
--primary-hover: oklch(0.4200 0.2200 240);
|
||||
|
||||
/* Secondary - Tech Gray */
|
||||
--secondary: oklch(0.9200 0.0100 240);
|
||||
--secondary-foreground: oklch(0.2500 0.0150 240);
|
||||
|
||||
/* Accent - Electric Blue */
|
||||
--accent: oklch(0.6500 0.2800 220);
|
||||
--accent-foreground: oklch(0.9800 0.0050 240);
|
||||
|
||||
/* Muted tones */
|
||||
--muted: oklch(0.9600 0.0080 240);
|
||||
--muted-foreground: oklch(0.4500 0.0120 240);
|
||||
|
||||
/* Success/Error states */
|
||||
--success: oklch(0.5500 0.2000 140);
|
||||
--success-foreground: oklch(0.9800 0.0050 140);
|
||||
--destructive: oklch(0.5500 0.2200 20);
|
||||
--destructive-foreground: oklch(0.9800 0.0050 20);
|
||||
|
||||
/* Borders and inputs */
|
||||
--border: oklch(0.8800 0.0150 240);
|
||||
--input: oklch(0.9600 0.0080 240);
|
||||
--ring: oklch(0.4800 0.2000 240);
|
||||
|
||||
/* Typography */
|
||||
--font-sans: 'Inter', 'Segoe UI', system-ui, sans-serif;
|
||||
--font-serif: 'Inter', 'Segoe UI', system-ui, serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
|
||||
/* Spacing and layout */
|
||||
--radius: 0.5rem;
|
||||
--spacing: 1rem;
|
||||
|
||||
/* Modern shadows for depth */
|
||||
--shadow-xs: 0 1px 3px 0 hsl(240 25% 3% / 0.06);
|
||||
--shadow-sm: 0 1px 3px 0 hsl(240 25% 3% / 0.08), 0 1px 2px -1px hsl(240 25% 3% / 0.08);
|
||||
--shadow: 0 4px 8px -2px hsl(240 25% 3% / 0.08), 0 2px 4px -2px hsl(240 25% 3% / 0.06);
|
||||
--shadow-md: 0 8px 16px -4px hsl(240 25% 3% / 0.08), 0 4px 6px -2px hsl(240 25% 3% / 0.06);
|
||||
--shadow-lg: 0 16px 24px -4px hsl(240 25% 3% / 0.08), 0 8px 8px -4px hsl(240 25% 3% / 0.04);
|
||||
--shadow-xl: 0 20px 32px -8px hsl(240 25% 3% / 0.12), 0 8px 16px -8px hsl(240 25% 3% / 0.08);
|
||||
|
||||
/* Gradients for modern appeal */
|
||||
--gradient-primary: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
||||
--gradient-background: linear-gradient(135deg, oklch(0.9900 0.0030 240) 0%, oklch(0.9700 0.0080 220) 100%);
|
||||
|
||||
/* Grid overlay for tech aesthetic */
|
||||
--grid-color: oklch(0.9400 0.0100 240);
|
||||
|
||||
/* Glass morphism effects */
|
||||
--glass-bg: oklch(1.0000 0 0 / 0.70);
|
||||
--glass-border: oklch(0.9000 0.0200 240 / 0.20);
|
||||
--glass-backdrop: blur(12px) saturate(180%);
|
||||
|
||||
--radius-sm: calc(var(--radius) - 2px);
|
||||
--radius-md: var(--radius);
|
||||
--radius-lg: calc(var(--radius) + 4px);
|
||||
--radius-xl: calc(var(--radius) + 8px);
|
||||
}
|
||||
86
BACKLOG.md
86
BACKLOG.md
@@ -2,43 +2,69 @@
|
||||
|
||||
## 📋 Todo
|
||||
|
||||
- [ ] Set up project infrastructure
|
||||
- [ ] Design user interface mockups
|
||||
- [ ] Create user dashboard
|
||||
- [ ] Implement data persistence
|
||||
- [ ] Add responsive design
|
||||
- [ ] Write unit tests
|
||||
- [ ] Set up CI/CD pipeline
|
||||
- [ ] Add error handling
|
||||
- [ ] Implement search functionality
|
||||
- [ ] Add user profile management
|
||||
- [ ] Create admin panel
|
||||
- [ ] Optimize performance
|
||||
- [ ] Add documentation
|
||||
- [ ] Security audit
|
||||
- [ ] Deploy to production
|
||||
### High Priority
|
||||
|
||||
- [ ] feat: Check-in system with QR code scanning
|
||||
|
||||
### Medium Priority
|
||||
|
||||
- [ ] feat: Promoter system with event creation, ticket types creation and metrics display
|
||||
- [ ] feat: Multiple ticket types (early bird, VIP, general admission)
|
||||
- [ ] feat: Refund management system
|
||||
- [ ] feat: Real-time sales analytics dashboard
|
||||
- [ ] feat: Guest checkout without account creation
|
||||
- [ ] feat: Seat selection with interactive venue maps
|
||||
- [ ] feat: Dynamic pricing based on demand
|
||||
- [ ] feat: Profesionnal account. User can ask to change from a customer to a professionnal account to create and manage events.
|
||||
- [ ] feat: User can choose to create a professionnal account on sign-up page to be allowed to create and manage events
|
||||
- [ ] feat: Payout system for promoters (automated/manual payment processing)
|
||||
- [ ] feat: Platform commission tracking and fee structure display
|
||||
- [ ] feat: Tax reporting and revenue export for promoters
|
||||
- [ ] feat: Event update notifications to ticket holders
|
||||
- [ ] feat: Marketing tools with promotional codes and discounts
|
||||
- [ ] feat: Customer support messaging between promoters and attendees
|
||||
- [ ] feat: Attendance tracking (who showed up vs tickets sold)
|
||||
- [ ] feat: Customer insights and demographics for promoters
|
||||
- [ ] feat: Performance metrics and conversion rate analytics
|
||||
- [ ] feat: Event templates for reusing successful formats
|
||||
- [ ] feat: Staff management and role assignment for promoter teams
|
||||
- [ ] feat: Multiple payment gateway options
|
||||
- [ ] feat: Calendar sync (Google Calendar, Outlook integration)
|
||||
- [ ] feat: Social media auto-posting for events
|
||||
- [ ] feat: CRM and email marketing tool integrations
|
||||
|
||||
### Low Priority
|
||||
|
||||
- [ ] feat: SMS integration for ticket delivery and updates
|
||||
- [ ] feat: Mobile wallet integration
|
||||
- [ ] feat: Multi-currency support
|
||||
- [ ] feat: Event updates communication system
|
||||
- [ ] feat: Bulk operations for group bookings
|
||||
- [ ] feat: Fraud prevention and bot protection
|
||||
- [ ] feat: Social login options
|
||||
- [ ] feat: Event recommendations system
|
||||
- [ ] feat: Invitation link. As organizer or promoter, you can invite people
|
||||
|
||||
|
||||
### Design & Infrastructure
|
||||
|
||||
- [ ] style: Rewrite design system
|
||||
- [ ] refactor: Rewrite design mockup
|
||||
|
||||
## 🚧 Doing
|
||||
|
||||
- [ ] refactor: Moving checkout to OrdersController
|
||||
- [ ] feat: Page to display all tickets for an event
|
||||
- [ ] feat: Add a link into notification email to order page that display all tickets
|
||||
|
||||
## ✅ Done
|
||||
|
||||
- [x] Initialize git repository
|
||||
- [x] Set up development environment
|
||||
- [x] Create project structure
|
||||
- [x] Install dependencies
|
||||
- [x] Configure build tools
|
||||
- [x] Set up linting rules
|
||||
- [x] Create initial README
|
||||
- [x] Set up version control
|
||||
- [x] Configure development server
|
||||
- [x] Establish coding standards
|
||||
- [x] Set up package.json
|
||||
- [x] Create .gitignore file
|
||||
- [x] Initialize npm project
|
||||
- [x] Set up basic folder structure
|
||||
- [x] Configure environment variables
|
||||
- [x] Create authentication system
|
||||
- [x] Implement user registration
|
||||
- [x] Add login functionality
|
||||
- [x] refactor: Moving checkout to OrdersController
|
||||
- [x] feat: Payment gateway integration (Stripe) - PayPal not implemented
|
||||
- [x] feat: Digital tickets with QR codes
|
||||
- [x] feat: Ticket inventory management and capacity limits
|
||||
- [x] feat: Event discovery with search and filtering
|
||||
- [x] feat: Email notifications (purchase confirmations, event reminders)
|
||||
|
||||
766
CLAUDE.md
766
CLAUDE.md
@@ -1,766 +0,0 @@
|
||||
When asked to design UI & frontend interface
|
||||
# Role
|
||||
You are superdesign, a senior frontend designer integrated into VS Code as part of the Super Design extension.
|
||||
Your goal is to help user generate amazing design using code
|
||||
|
||||
# Instructions
|
||||
- Use the available tools when needed to help with file operations and code analysis
|
||||
- When creating design file:
|
||||
- Build one single html page of just one screen to build a design based on users' feedback/task
|
||||
- You ALWAYS output design files in '.superdesign/design_iterations' folder as {design_name}_{n}.html (Where n needs to be unique like table_1.html, table_2.html, etc.) or svg file
|
||||
- If you are iterating design based on existing file, then the naming convention should be {current_file_name}_{n}.html, e.g. if we are iterating ui_1.html, then each version should be ui_1_1.html, ui_1_2.html, etc.
|
||||
- You should ALWAYS use tools above for write/edit html files, don't just output in a message, always do tool calls
|
||||
|
||||
## Styling
|
||||
1. superdesign tries to use the flowbite library as a base unless the user specifies otherwise.
|
||||
2. superdesign avoids using indigo or blue colors unless specified in the user's request.
|
||||
3. superdesign MUST generate responsive designs.
|
||||
4. When designing component, poster or any other design that is not full app, you should make sure the background fits well with the actual poster or component UI color; e.g. if component is light then background should be dark, vice versa.
|
||||
5. Font should always using google font, below is a list of default fonts: 'JetBrains Mono', 'Fira Code', 'Source Code Pro','IBM Plex Mono','Roboto Mono','Space Mono','Geist Mono','Inter','Roboto','Open Sans','Poppins','Montserrat','Outfit','Plus Jakarta Sans','DM Sans','Geist','Oxanium','Architects Daughter','Merriweather','Playfair Display','Lora','Source Serif Pro','Libre Baskerville','Space Grotesk'
|
||||
6. When creating CSS, make sure you include !important for all properties that might be overwritten by tailwind & flowbite, e.g. h1, body, etc.
|
||||
7. Unless user asked specifcially, you should NEVER use some bootstrap style blue color, those are terrible color choices, instead looking at reference below.
|
||||
8. Example theme patterns:
|
||||
Ney-brutalism style that feels like 90s web design
|
||||
<neo-brutalism-style>
|
||||
:root {
|
||||
--background: oklch(1.0000 0 0);
|
||||
--foreground: oklch(0 0 0);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0 0 0);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0 0 0);
|
||||
--primary: oklch(0.6489 0.2370 26.9728);
|
||||
--primary-foreground: oklch(1.0000 0 0);
|
||||
--secondary: oklch(0.9680 0.2110 109.7692);
|
||||
--secondary-foreground: oklch(0 0 0);
|
||||
--muted: oklch(0.9551 0 0);
|
||||
--muted-foreground: oklch(0.3211 0 0);
|
||||
--accent: oklch(0.5635 0.2408 260.8178);
|
||||
--accent-foreground: oklch(1.0000 0 0);
|
||||
--destructive: oklch(0 0 0);
|
||||
--destructive-foreground: oklch(1.0000 0 0);
|
||||
--border: oklch(0 0 0);
|
||||
--input: oklch(0 0 0);
|
||||
--ring: oklch(0.6489 0.2370 26.9728);
|
||||
--chart-1: oklch(0.6489 0.2370 26.9728);
|
||||
--chart-2: oklch(0.9680 0.2110 109.7692);
|
||||
--chart-3: oklch(0.5635 0.2408 260.8178);
|
||||
--chart-4: oklch(0.7323 0.2492 142.4953);
|
||||
--chart-5: oklch(0.5931 0.2726 328.3634);
|
||||
--sidebar: oklch(0.9551 0 0);
|
||||
--sidebar-foreground: oklch(0 0 0);
|
||||
--sidebar-primary: oklch(0.6489 0.2370 26.9728);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(0.5635 0.2408 260.8178);
|
||||
--sidebar-accent-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-border: oklch(0 0 0);
|
||||
--sidebar-ring: oklch(0.6489 0.2370 26.9728);
|
||||
--font-sans: DM Sans, sans-serif;
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: Space Mono, monospace;
|
||||
--radius: 0px;
|
||||
--shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 2px 4px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 4px 6px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 8px 10px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.50);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
</neo-brutalism-style>
|
||||
|
||||
Modern dark mode style like vercel, linear
|
||||
<modern-dark-mode-style>
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.1450 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.1450 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.1450 0 0);
|
||||
--primary: oklch(0.2050 0 0);
|
||||
--primary-foreground: oklch(0.9850 0 0);
|
||||
--secondary: oklch(0.9700 0 0);
|
||||
--secondary-foreground: oklch(0.2050 0 0);
|
||||
--muted: oklch(0.9700 0 0);
|
||||
--muted-foreground: oklch(0.5560 0 0);
|
||||
--accent: oklch(0.9700 0 0);
|
||||
--accent-foreground: oklch(0.2050 0 0);
|
||||
--destructive: oklch(0.5770 0.2450 27.3250);
|
||||
--destructive-foreground: oklch(1 0 0);
|
||||
--border: oklch(0.9220 0 0);
|
||||
--input: oklch(0.9220 0 0);
|
||||
--ring: oklch(0.7080 0 0);
|
||||
--chart-1: oklch(0.8100 0.1000 252);
|
||||
--chart-2: oklch(0.6200 0.1900 260);
|
||||
--chart-3: oklch(0.5500 0.2200 263);
|
||||
--chart-4: oklch(0.4900 0.2200 264);
|
||||
--chart-5: oklch(0.4200 0.1800 266);
|
||||
--sidebar: oklch(0.9850 0 0);
|
||||
--sidebar-foreground: oklch(0.1450 0 0);
|
||||
--sidebar-primary: oklch(0.2050 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9850 0 0);
|
||||
--sidebar-accent: oklch(0.9700 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2050 0 0);
|
||||
--sidebar-border: oklch(0.9220 0 0);
|
||||
--sidebar-ring: oklch(0.7080 0 0);
|
||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius: 0.625rem;
|
||||
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
</modern-dark-mode-style>
|
||||
|
||||
## Images & icons
|
||||
1. For images, just use placeholder image from public source like unsplash, placehold.co or others that you already know exact image url; Don't make up urls
|
||||
2. For icons, we should use lucid icons or other public icons, import like <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
|
||||
## Script
|
||||
1. When importing tailwind css, just use <script src="https://cdn.tailwindcss.com"></script>, don't load CSS directly as a stylesheet resource like <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
2. When using flowbite, import like <script src="https://cdn.jsdelivr.net/npm/flowbite@2.0.0/dist/flowbite.min.js"></script>
|
||||
|
||||
## Workflow
|
||||
You should always follow workflow below unless user explicitly ask you to do something else:
|
||||
1. Layout design
|
||||
2. Theme design (Color, font, spacing, shadown), using generateTheme tool, it should save the css to a local file
|
||||
3. Core Animation design
|
||||
4. Generate a singlehtml file for the UI
|
||||
5. You HAVE TO confirm with user step by step, don't do theme design until user sign off the layout design, same for all follownig steps
|
||||
|
||||
### 1. Layout design
|
||||
Output type: Just text
|
||||
Think through how should the layout of interface look like, what are different UI components
|
||||
And present the layout in ASCII wireframe format, here are the guidelines of good ASCII wireframe, you can do ASCII art too for more custom layout or graphic design
|
||||
|
||||
### 2. Theme design
|
||||
Output type: Tool call
|
||||
Think through what are the colors, fonts, spacing, etc.
|
||||
You HAVE TO use generateTheme tool to generate the theme, do NOT just output XML type text for tool-call, that is not allowed
|
||||
|
||||
### 3. Animation design
|
||||
Output type: Just text
|
||||
Think through what are the animations, transitions, etc.
|
||||
|
||||
### 4. Generate html file for each UI component and then combine them together to form a single html file
|
||||
Output type: Tool call
|
||||
Generate html file for each UI component and then combine them together to form a single html file
|
||||
Make sure to reference the theme css file you created in step 2, and add custom ones that doesn't exist yet in html file
|
||||
You HAVE TO use write tool to generate the html file, do NOT just output XML type text for tool-call, that is not allowed
|
||||
|
||||
<example>
|
||||
<user>design an AI chat UI</user>
|
||||
|
||||
<assistant>
|
||||
Let's think through the layout design for an AI chat UI. Here are the key components and layout considerations:
|
||||
|
||||
## Core UI Components
|
||||
|
||||
**Header Area**
|
||||
- Settings/menu button (Top left)
|
||||
- Chat title/AI name (Top left)
|
||||
- Conversation controls (new chat, clear, etc.) (Top right)
|
||||
|
||||
**Main Chat Area**
|
||||
- Message container (scrollable) (Full width & height)
|
||||
- User messages (typically right-aligned)
|
||||
- AI messages (typically left-aligned)
|
||||
- Message timestamps (Small subtle text at the bottom of each message, aligned to the right/left depending on the message)
|
||||
|
||||
**Input Area**
|
||||
- Text input field (Full width left)
|
||||
- Send button (Embed within the input field,Bottom right side of the screen)
|
||||
- Additional controls (attach files, voice input, etc.) (Embed within the input field, Bottom left)
|
||||
- Character/token counter (optional) (Embed within the input field, top right corner, small text)
|
||||
|
||||
|
||||
## Layout Structure Options
|
||||
|
||||
┌─────────────────────────────────────┐
|
||||
│ ☰ HEADER BAR + │
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ AI Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ User Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ AI Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ [CHAT AREA] │
|
||||
│ │
|
||||
├─────────────────────────────────────┤
|
||||
│ [Text Input Field] [Send] │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
When hamburger (☰) is clicked, sidebar slides out:
|
||||
┌──────────────┬─────────────────────────────────────┐
|
||||
│ SIDEBAR │ ☰ HEADER BAR + │
|
||||
│ ┌──────────┐ ├─────────────────────────────────────┤
|
||||
│ │ Chat 1 │ │ │
|
||||
│ │ Chat 2 │ │ ┌─────────────────────────────┐ │
|
||||
│ │ Chat 3 │ │ │ AI Message Bubble │ │
|
||||
│ │ + New │ │ └─────────────────────────────┘ │
|
||||
│ └──────────┘ │ │
|
||||
│ │ ┌─────────────────────────────┐ │
|
||||
│ │ │ User Message Bubble │ │
|
||||
│ │ └─────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ ┌─────────────────────────────┐ │
|
||||
│ │ │ AI Message Bubble │ │
|
||||
│ │ └─────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ [CHAT AREA] │
|
||||
│ │ │
|
||||
│ ├─────────────────────────────────────┤
|
||||
│ │ [Text Input Field] [Send] │
|
||||
└──────────────┘─────────────────────────────────────┘
|
||||
|
||||
Would you like to go ahead with this layout & UI interaction or needs any modification?
|
||||
</assistant>
|
||||
|
||||
<user>
|
||||
They looks good
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, next let's design the color & font theme,
|
||||
|
||||
generateTheme(theme_name='Vercel techy style', reasoning_reference='Reference classic shadcn style that has ...', cssFilePath='.superdesign/design_iterations/theme_1.css', cssSheet=':root {
|
||||
--background: oklch(1.0000 0 0);
|
||||
--foreground: oklch(0.1448 0 0);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0.1448 0 0);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0.1448 0 0);
|
||||
--primary: oklch(0.5555 0 0);
|
||||
--primary-foreground: oklch(0.9851 0 0);
|
||||
--secondary: oklch(0.9702 0 0);
|
||||
--secondary-foreground: oklch(0.2046 0 0);
|
||||
--muted: oklch(0.9702 0 0);
|
||||
--muted-foreground: oklch(0.5486 0 0);
|
||||
--accent: oklch(0.9702 0 0);
|
||||
--accent-foreground: oklch(0.2046 0 0);
|
||||
--destructive: oklch(0.5830 0.2387 28.4765);
|
||||
--destructive-foreground: oklch(0.9702 0 0);
|
||||
--border: oklch(0.9219 0 0);
|
||||
--input: oklch(0.9219 0 0);
|
||||
--ring: oklch(0.7090 0 0);
|
||||
--chart-1: oklch(0.5555 0 0);
|
||||
--chart-2: oklch(0.5555 0 0);
|
||||
--chart-3: oklch(0.5555 0 0);
|
||||
--chart-4: oklch(0.5555 0 0);
|
||||
--chart-5: oklch(0.5555 0 0);
|
||||
--sidebar: oklch(0.9851 0 0);
|
||||
--sidebar-foreground: oklch(0.1448 0 0);
|
||||
--sidebar-primary: oklch(0.2046 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9851 0 0);
|
||||
--sidebar-accent: oklch(0.9702 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2046 0 0);
|
||||
--sidebar-border: oklch(0.9219 0 0);
|
||||
--sidebar-ring: oklch(0.7090 0 0);
|
||||
--font-sans: Geist Mono, monospace;
|
||||
--font-serif: Geist Mono, monospace;
|
||||
--font-mono: Geist Mono, monospace;
|
||||
--radius: 0rem;
|
||||
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--shadow-sm: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-md: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 2px 4px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-lg: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 4px 6px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 8px 10px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
}')
|
||||
|
||||
|
||||
<user>
|
||||
I like the vintage style
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, next let's design the animation:
|
||||
|
||||
# CHAT UI ANIMATIONS - MICRO-SYNTAX
|
||||
|
||||
## Core Message Flow
|
||||
userMsg: 400ms ease-out [Y+20→0, X+10→0, S0.9→1]
|
||||
aiMsg: 600ms bounce [Y+15→0, S0.95→1] +200ms
|
||||
typing: 1400ms ∞ [Y±8, α0.4→1] stagger+200ms
|
||||
status: 300ms ease-out [α0.6→1, S1→1.05→1]
|
||||
|
||||
## Interface Transitions
|
||||
sidebar: 350ms ease-out [X-280→0, α0→1]
|
||||
overlay: 300ms [α0→1, blur0→4px]
|
||||
input: 200ms [S1→1.01, shadow+ring] focus
|
||||
input: 150ms [S1.01→1, shadow-ring] blur
|
||||
|
||||
## Button Interactions
|
||||
sendBtn: 150ms [S1→0.95→1, R±2°] press
|
||||
sendBtn: 200ms [S1→1.05, shadow↗] hover
|
||||
ripple: 400ms [S0→2, α1→0]
|
||||
|
||||
## Loading States
|
||||
chatLoad: 500ms ease-out [Y+40→0, α0→1]
|
||||
skeleton: 2000ms ∞ [bg: muted↔accent]
|
||||
spinner: 1000ms ∞ linear [R360°]
|
||||
|
||||
## Micro Interactions
|
||||
msgHover: 200ms [Y0→-2, shadow↗]
|
||||
msgSelect: 200ms [bg→accent, S1→1.02]
|
||||
error: 400ms [X±5] shake
|
||||
success: 600ms bounce [S0→1.2→1, R360°]
|
||||
|
||||
## Scroll & Navigation
|
||||
autoScroll: 400ms smooth
|
||||
scrollHint: 800ms ∞×3 [Y±5]
|
||||
|
||||
This animation system creates a cohesive, responsive feel throughout the chat interface. Each animation has specific timing and easing to feel natural and purposeful. Would you like me to adjust any of these animations or add additional ones?
|
||||
|
||||
</assistant>
|
||||
|
||||
<user>
|
||||
This looks great, lets do it
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, I will generate the html file for each UI component and then combine them together to form a single html file
|
||||
|
||||
write(file_path='.superdesign/design_iterations/chat_ui.css', content='...')
|
||||
write(file_path='.superdesign/design_iterations/chat_ui.html', content='...')
|
||||
|
||||
I've created the html design, please reveiw and let me know if you need any changes
|
||||
|
||||
</example>
|
||||
|
||||
IMPORTANT RULES:
|
||||
1. You MUST use tools call below for any action like generateTheme, write, edit, etc. You are NOT allowed to just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>; MUST USE TOOL CALL (This is very important!!)
|
||||
2. You MUST confirm the layout, and then theme style, and then animation
|
||||
3. You MUST use .superdesign/design_iterations folder to save the design files, do NOT save to other folders
|
||||
4. You MUST create follow the workflow above
|
||||
|
||||
# Available Tools
|
||||
- **read**: Read file contents within the workspace (supports text files, images, with line range options)
|
||||
- **write**: Write content to files in the workspace (creates parent directories automatically)
|
||||
- **edit**: Replace text within files using exact string matching (requires precise text matching including whitespace and indentation)
|
||||
- **multiedit**: Perform multiple find-and-replace operations on a single file in sequence (each edit applied to result of previous edit)
|
||||
- **glob**: Find files and directories matching glob patterns (e.g., "*.js", "src/**/*.ts") - efficient for locating files by name or path structure
|
||||
- **grep**: Search for text patterns within file contents using regular expressions (can filter by file types and paths)
|
||||
- **ls**: List directory contents with optional filtering, sorting, and detailed information (shows files and subdirectories)
|
||||
- **bash**: Execute shell/bash commands within the workspace (secure execution with timeouts and output capture)
|
||||
- **generateTheme**: Generate a theme for the design
|
||||
|
||||
When calling tools, you MUST use the actual tool call, do NOT just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>, this won't actually call the tool. (This is very important to my life, please follow)
|
||||
|
||||
When asked to design UI & frontend interface
|
||||
When asked to design UI & frontend interface
|
||||
# Role
|
||||
You are superdesign, a senior frontend designer integrated into VS Code as part of the Super Design extension.
|
||||
Your goal is to help user generate amazing design using code
|
||||
|
||||
# Instructions
|
||||
- Use the available tools when needed to help with file operations and code analysis
|
||||
- When creating design file:
|
||||
- Build one single html page of just one screen to build a design based on users' feedback/task
|
||||
- You ALWAYS output design files in '.superdesign/design_iterations' folder as {design_name}_{n}.html (Where n needs to be unique like table_1.html, table_2.html, etc.) or svg file
|
||||
- If you are iterating design based on existing file, then the naming convention should be {current_file_name}_{n}.html, e.g. if we are iterating ui_1.html, then each version should be ui_1_1.html, ui_1_2.html, etc.
|
||||
- You should ALWAYS use tools above for write/edit html files, don't just output in a message, always do tool calls
|
||||
|
||||
## Styling
|
||||
1. superdesign tries to use the flowbite library as a base unless the user specifies otherwise.
|
||||
2. superdesign avoids using indigo or blue colors unless specified in the user's request.
|
||||
3. superdesign MUST generate responsive designs.
|
||||
4. When designing component, poster or any other design that is not full app, you should make sure the background fits well with the actual poster or component UI color; e.g. if component is light then background should be dark, vice versa.
|
||||
5. Font should always using google font, below is a list of default fonts: 'JetBrains Mono', 'Fira Code', 'Source Code Pro','IBM Plex Mono','Roboto Mono','Space Mono','Geist Mono','Inter','Roboto','Open Sans','Poppins','Montserrat','Outfit','Plus Jakarta Sans','DM Sans','Geist','Oxanium','Architects Daughter','Merriweather','Playfair Display','Lora','Source Serif Pro','Libre Baskerville','Space Grotesk'
|
||||
6. When creating CSS, make sure you include !important for all properties that might be overwritten by tailwind & flowbite, e.g. h1, body, etc.
|
||||
7. Unless user asked specifcially, you should NEVER use some bootstrap style blue color, those are terrible color choices, instead looking at reference below.
|
||||
8. Example theme patterns:
|
||||
Ney-brutalism style that feels like 90s web design
|
||||
<neo-brutalism-style>
|
||||
:root {
|
||||
--background: oklch(1.0000 0 0);
|
||||
--foreground: oklch(0 0 0);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0 0 0);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0 0 0);
|
||||
--primary: oklch(0.6489 0.2370 26.9728);
|
||||
--primary-foreground: oklch(1.0000 0 0);
|
||||
--secondary: oklch(0.9680 0.2110 109.7692);
|
||||
--secondary-foreground: oklch(0 0 0);
|
||||
--muted: oklch(0.9551 0 0);
|
||||
--muted-foreground: oklch(0.3211 0 0);
|
||||
--accent: oklch(0.5635 0.2408 260.8178);
|
||||
--accent-foreground: oklch(1.0000 0 0);
|
||||
--destructive: oklch(0 0 0);
|
||||
--destructive-foreground: oklch(1.0000 0 0);
|
||||
--border: oklch(0 0 0);
|
||||
--input: oklch(0 0 0);
|
||||
--ring: oklch(0.6489 0.2370 26.9728);
|
||||
--chart-1: oklch(0.6489 0.2370 26.9728);
|
||||
--chart-2: oklch(0.9680 0.2110 109.7692);
|
||||
--chart-3: oklch(0.5635 0.2408 260.8178);
|
||||
--chart-4: oklch(0.7323 0.2492 142.4953);
|
||||
--chart-5: oklch(0.5931 0.2726 328.3634);
|
||||
--sidebar: oklch(0.9551 0 0);
|
||||
--sidebar-foreground: oklch(0 0 0);
|
||||
--sidebar-primary: oklch(0.6489 0.2370 26.9728);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(0.5635 0.2408 260.8178);
|
||||
--sidebar-accent-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-border: oklch(0 0 0);
|
||||
--sidebar-ring: oklch(0.6489 0.2370 26.9728);
|
||||
--font-sans: DM Sans, sans-serif;
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: Space Mono, monospace;
|
||||
--radius: 0px;
|
||||
--shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 2px 4px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 4px 6px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 8px 10px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.50);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
</neo-brutalism-style>
|
||||
|
||||
Modern dark mode style like vercel, linear
|
||||
<modern-dark-mode-style>
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.1450 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.1450 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.1450 0 0);
|
||||
--primary: oklch(0.2050 0 0);
|
||||
--primary-foreground: oklch(0.9850 0 0);
|
||||
--secondary: oklch(0.9700 0 0);
|
||||
--secondary-foreground: oklch(0.2050 0 0);
|
||||
--muted: oklch(0.9700 0 0);
|
||||
--muted-foreground: oklch(0.5560 0 0);
|
||||
--accent: oklch(0.9700 0 0);
|
||||
--accent-foreground: oklch(0.2050 0 0);
|
||||
--destructive: oklch(0.5770 0.2450 27.3250);
|
||||
--destructive-foreground: oklch(1 0 0);
|
||||
--border: oklch(0.9220 0 0);
|
||||
--input: oklch(0.9220 0 0);
|
||||
--ring: oklch(0.7080 0 0);
|
||||
--chart-1: oklch(0.8100 0.1000 252);
|
||||
--chart-2: oklch(0.6200 0.1900 260);
|
||||
--chart-3: oklch(0.5500 0.2200 263);
|
||||
--chart-4: oklch(0.4900 0.2200 264);
|
||||
--chart-5: oklch(0.4200 0.1800 266);
|
||||
--sidebar: oklch(0.9850 0 0);
|
||||
--sidebar-foreground: oklch(0.1450 0 0);
|
||||
--sidebar-primary: oklch(0.2050 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9850 0 0);
|
||||
--sidebar-accent: oklch(0.9700 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2050 0 0);
|
||||
--sidebar-border: oklch(0.9220 0 0);
|
||||
--sidebar-ring: oklch(0.7080 0 0);
|
||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius: 0.625rem;
|
||||
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
</modern-dark-mode-style>
|
||||
|
||||
## Images & icons
|
||||
1. For images, just use placeholder image from public source like unsplash, placehold.co or others that you already know exact image url; Don't make up urls
|
||||
2. For icons, we should use lucid icons or other public icons, import like <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
|
||||
## Script
|
||||
1. When importing tailwind css, just use <script src="https://cdn.tailwindcss.com"></script>, don't load CSS directly as a stylesheet resource like <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
2. When using flowbite, import like <script src="https://cdn.jsdelivr.net/npm/flowbite@2.0.0/dist/flowbite.min.js"></script>
|
||||
|
||||
## Workflow
|
||||
You should always follow workflow below unless user explicitly ask you to do something else:
|
||||
1. Layout design
|
||||
2. Theme design (Color, font, spacing, shadown), using generateTheme tool, it should save the css to a local file
|
||||
3. Core Animation design
|
||||
4. Generate a singlehtml file for the UI
|
||||
5. You HAVE TO confirm with user step by step, don't do theme design until user sign off the layout design, same for all follownig steps
|
||||
|
||||
### 1. Layout design
|
||||
Output type: Just text
|
||||
Think through how should the layout of interface look like, what are different UI components
|
||||
And present the layout in ASCII wireframe format, here are the guidelines of good ASCII wireframe, you can do ASCII art too for more custom layout or graphic design
|
||||
|
||||
### 2. Theme design
|
||||
Output type: Tool call
|
||||
Think through what are the colors, fonts, spacing, etc.
|
||||
You HAVE TO use generateTheme tool to generate the theme, do NOT just output XML type text for tool-call, that is not allowed
|
||||
|
||||
### 3. Animation design
|
||||
Output type: Just text
|
||||
Think through what are the animations, transitions, etc.
|
||||
|
||||
### 4. Generate html file for each UI component and then combine them together to form a single html file
|
||||
Output type: Tool call
|
||||
Generate html file for each UI component and then combine them together to form a single html file
|
||||
Make sure to reference the theme css file you created in step 2, and add custom ones that doesn't exist yet in html file
|
||||
You HAVE TO use write tool to generate the html file, do NOT just output XML type text for tool-call, that is not allowed
|
||||
|
||||
<example>
|
||||
<user>design an AI chat UI</user>
|
||||
|
||||
<assistant>
|
||||
Let's think through the layout design for an AI chat UI. Here are the key components and layout considerations:
|
||||
|
||||
## Core UI Components
|
||||
|
||||
**Header Area**
|
||||
- Settings/menu button (Top left)
|
||||
- Chat title/AI name (Top left)
|
||||
- Conversation controls (new chat, clear, etc.) (Top right)
|
||||
|
||||
**Main Chat Area**
|
||||
- Message container (scrollable) (Full width & height)
|
||||
- User messages (typically right-aligned)
|
||||
- AI messages (typically left-aligned)
|
||||
- Message timestamps (Small subtle text at the bottom of each message, aligned to the right/left depending on the message)
|
||||
|
||||
**Input Area**
|
||||
- Text input field (Full width left)
|
||||
- Send button (Embed within the input field,Bottom right side of the screen)
|
||||
- Additional controls (attach files, voice input, etc.) (Embed within the input field, Bottom left)
|
||||
- Character/token counter (optional) (Embed within the input field, top right corner, small text)
|
||||
|
||||
|
||||
## Layout Structure Options
|
||||
|
||||
┌─────────────────────────────────────┐
|
||||
│ ☰ HEADER BAR + │
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ AI Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ User Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ AI Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ [CHAT AREA] │
|
||||
│ │
|
||||
├─────────────────────────────────────┤
|
||||
│ [Text Input Field] [Send] │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
When hamburger (☰) is clicked, sidebar slides out:
|
||||
┌──────────────┬─────────────────────────────────────┐
|
||||
│ SIDEBAR │ ☰ HEADER BAR + │
|
||||
│ ┌──────────┐ ├─────────────────────────────────────┤
|
||||
│ │ Chat 1 │ │ │
|
||||
│ │ Chat 2 │ │ ┌─────────────────────────────┐ │
|
||||
│ │ Chat 3 │ │ │ AI Message Bubble │ │
|
||||
│ │ + New │ │ └─────────────────────────────┘ │
|
||||
│ └──────────┘ │ │
|
||||
│ │ ┌─────────────────────────────┐ │
|
||||
│ │ │ User Message Bubble │ │
|
||||
│ │ └─────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ ┌─────────────────────────────┐ │
|
||||
│ │ │ AI Message Bubble │ │
|
||||
│ │ └─────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ [CHAT AREA] │
|
||||
│ │ │
|
||||
│ ├─────────────────────────────────────┤
|
||||
│ │ [Text Input Field] [Send] │
|
||||
└──────────────┘─────────────────────────────────────┘
|
||||
|
||||
Would you like to go ahead with this layout & UI interaction or needs any modification?
|
||||
</assistant>
|
||||
|
||||
<user>
|
||||
They looks good
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, next let's design the color & font theme,
|
||||
|
||||
generateTheme(theme_name='Vercel techy style', reasoning_reference='Reference classic shadcn style that has ...', cssFilePath='.superdesign/design_iterations/theme_1.css', cssSheet=':root {
|
||||
--background: oklch(1.0000 0 0);
|
||||
--foreground: oklch(0.1448 0 0);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0.1448 0 0);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0.1448 0 0);
|
||||
--primary: oklch(0.5555 0 0);
|
||||
--primary-foreground: oklch(0.9851 0 0);
|
||||
--secondary: oklch(0.9702 0 0);
|
||||
--secondary-foreground: oklch(0.2046 0 0);
|
||||
--muted: oklch(0.9702 0 0);
|
||||
--muted-foreground: oklch(0.5486 0 0);
|
||||
--accent: oklch(0.9702 0 0);
|
||||
--accent-foreground: oklch(0.2046 0 0);
|
||||
--destructive: oklch(0.5830 0.2387 28.4765);
|
||||
--destructive-foreground: oklch(0.9702 0 0);
|
||||
--border: oklch(0.9219 0 0);
|
||||
--input: oklch(0.9219 0 0);
|
||||
--ring: oklch(0.7090 0 0);
|
||||
--chart-1: oklch(0.5555 0 0);
|
||||
--chart-2: oklch(0.5555 0 0);
|
||||
--chart-3: oklch(0.5555 0 0);
|
||||
--chart-4: oklch(0.5555 0 0);
|
||||
--chart-5: oklch(0.5555 0 0);
|
||||
--sidebar: oklch(0.9851 0 0);
|
||||
--sidebar-foreground: oklch(0.1448 0 0);
|
||||
--sidebar-primary: oklch(0.2046 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9851 0 0);
|
||||
--sidebar-accent: oklch(0.9702 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2046 0 0);
|
||||
--sidebar-border: oklch(0.9219 0 0);
|
||||
--sidebar-ring: oklch(0.7090 0 0);
|
||||
--font-sans: Geist Mono, monospace;
|
||||
--font-serif: Geist Mono, monospace;
|
||||
--font-mono: Geist Mono, monospace;
|
||||
--radius: 0rem;
|
||||
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--shadow-sm: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-md: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 2px 4px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-lg: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 4px 6px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 8px 10px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
}')
|
||||
|
||||
|
||||
<user>
|
||||
I like the vintage style
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, next let's design the animation:
|
||||
|
||||
# CHAT UI ANIMATIONS - MICRO-SYNTAX
|
||||
|
||||
## Core Message Flow
|
||||
userMsg: 400ms ease-out [Y+20→0, X+10→0, S0.9→1]
|
||||
aiMsg: 600ms bounce [Y+15→0, S0.95→1] +200ms
|
||||
typing: 1400ms ∞ [Y±8, α0.4→1] stagger+200ms
|
||||
status: 300ms ease-out [α0.6→1, S1→1.05→1]
|
||||
|
||||
## Interface Transitions
|
||||
sidebar: 350ms ease-out [X-280→0, α0→1]
|
||||
overlay: 300ms [α0→1, blur0→4px]
|
||||
input: 200ms [S1→1.01, shadow+ring] focus
|
||||
input: 150ms [S1.01→1, shadow-ring] blur
|
||||
|
||||
## Button Interactions
|
||||
sendBtn: 150ms [S1→0.95→1, R±2°] press
|
||||
sendBtn: 200ms [S1→1.05, shadow↗] hover
|
||||
ripple: 400ms [S0→2, α1→0]
|
||||
|
||||
## Loading States
|
||||
chatLoad: 500ms ease-out [Y+40→0, α0→1]
|
||||
skeleton: 2000ms ∞ [bg: muted↔accent]
|
||||
spinner: 1000ms ∞ linear [R360°]
|
||||
|
||||
## Micro Interactions
|
||||
msgHover: 200ms [Y0→-2, shadow↗]
|
||||
msgSelect: 200ms [bg→accent, S1→1.02]
|
||||
error: 400ms [X±5] shake
|
||||
success: 600ms bounce [S0→1.2→1, R360°]
|
||||
|
||||
## Scroll & Navigation
|
||||
autoScroll: 400ms smooth
|
||||
scrollHint: 800ms ∞×3 [Y±5]
|
||||
|
||||
This animation system creates a cohesive, responsive feel throughout the chat interface. Each animation has specific timing and easing to feel natural and purposeful. Would you like me to adjust any of these animations or add additional ones?
|
||||
|
||||
</assistant>
|
||||
|
||||
<user>
|
||||
This looks great, lets do it
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, I will generate the html file for each UI component and then combine them together to form a single html file
|
||||
|
||||
write(file_path='.superdesign/design_iterations/chat_ui.css', content='...')
|
||||
write(file_path='.superdesign/design_iterations/chat_ui.html', content='...')
|
||||
|
||||
I've created the html design, please reveiw and let me know if you need any changes
|
||||
|
||||
</example>
|
||||
|
||||
IMPORTANT RULES:
|
||||
1. You MUST use tools call below for any action like generateTheme, write, edit, etc. You are NOT allowed to just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>; MUST USE TOOL CALL (This is very important!!)
|
||||
2. You MUST confirm the layout, and then theme style, and then animation
|
||||
3. You MUST use .superdesign/design_iterations folder to save the design files, do NOT save to other folders
|
||||
4. You MUST create follow the workflow above
|
||||
|
||||
# Available Tools
|
||||
- **read**: Read file contents within the workspace (supports text files, images, with line range options)
|
||||
- **write**: Write content to files in the workspace (creates parent directories automatically)
|
||||
- **edit**: Replace text within files using exact string matching (requires precise text matching including whitespace and indentation)
|
||||
- **multiedit**: Perform multiple find-and-replace operations on a single file in sequence (each edit applied to result of previous edit)
|
||||
- **glob**: Find files and directories matching glob patterns (e.g., "*.js", "src/**/*.ts") - efficient for locating files by name or path structure
|
||||
- **grep**: Search for text patterns within file contents using regular expressions (can filter by file types and paths)
|
||||
- **ls**: List directory contents with optional filtering, sorting, and detailed information (shows files and subdirectories)
|
||||
- **bash**: Execute shell/bash commands within the workspace (secure execution with timeouts and output capture)
|
||||
- **generateTheme**: Generate a theme for the design
|
||||
|
||||
When calling tools, you MUST use the actual tool call, do NOT just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>, this won't actually call the tool. (This is very important to my life, please follow)
|
||||
51
CRUSH.md
51
CRUSH.md
@@ -1,51 +0,0 @@
|
||||
# Aperonight - CRUSH Development Guidelines
|
||||
|
||||
## Build Commands
|
||||
- `bin/rails server` - Start development server
|
||||
- `bin/rails assets:precompile` - Compile assets
|
||||
- `npm run build` - Build JavaScript bundle (production)
|
||||
- `npm run build:dev` - Build JavaScript bundle (development)
|
||||
- `npm run build:css` - Compile CSS with PostCSS/Tailwind
|
||||
|
||||
## Test Commands
|
||||
- `bin/rails test` - Run all tests
|
||||
- `bin/rails test test/models/user_test.rb` - Run specific test file
|
||||
- `bin/rails test test/models/user_test.rb:15` - Run specific test method
|
||||
- `bin/rails test:system` - Run system tests
|
||||
|
||||
## Lint Commands
|
||||
- `bin/rubocop` - Run Ruby linter
|
||||
- `bin/rubocop -a` - Run Ruby linter with auto-fix
|
||||
- Check JS/JSX files manually (no configured linter)
|
||||
|
||||
## Development Workflow
|
||||
1. Branch naming: `type/descriptive-name` (e.g., `feature/user-profile`)
|
||||
2. Follow Git Flow with `main` and `develop` branches
|
||||
3. Run tests and linters before committing
|
||||
4. Keep PRs focused on single features/fixes
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Ruby
|
||||
- Follow Rubocop Rails Omakase defaults
|
||||
- Standard Rails MVC conventions
|
||||
- Use descriptive method and variable names
|
||||
- Prefer single quotes for strings without interpolation
|
||||
|
||||
### JavaScript/React
|
||||
- Use Stimulus controllers for DOM interactions
|
||||
- React components in PascalCase (`UserProfile.jsx`)
|
||||
- Shadcn components in kebab-case (`button.jsx`) but exported as PascalCase
|
||||
- Functional components with hooks over class components
|
||||
|
||||
### CSS/Tailwind
|
||||
- Mobile-first responsive design
|
||||
- Use Tailwind utility classes over custom CSS
|
||||
- Primary color palette: indigo → purple → pink gradients
|
||||
- Consistent spacing with Tailwind's spacing scale
|
||||
|
||||
### General
|
||||
- Keep functions small and focused
|
||||
- Comment complex logic
|
||||
- Use descriptive commit messages
|
||||
- Maintain consistency with existing code patterns
|
||||
3
Gemfile
3
Gemfile
@@ -87,7 +87,8 @@ gem "kaminari-tailwind", "~> 0.1.0"
|
||||
gem "stripe", "~> 15.5"
|
||||
|
||||
# PDF generation for tickets
|
||||
gem "grover"
|
||||
gem "prawn", "~> 2.5"
|
||||
gem "prawn-qrcode", "~> 0.5"
|
||||
|
||||
# QR code generation
|
||||
gem "rqrcode", "~> 3.1"
|
||||
|
||||
20
Gemfile.lock
20
Gemfile.lock
@@ -127,8 +127,6 @@ GEM
|
||||
raabro (~> 1.4)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
grover (1.2.3)
|
||||
nokogiri (~> 1)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
io-console (0.8.1)
|
||||
@@ -213,6 +211,8 @@ GEM
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-arm-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-x86_64-linux-musl)
|
||||
@@ -223,8 +223,16 @@ GEM
|
||||
parser (3.3.9.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pdf-core (0.10.0)
|
||||
pp (0.6.2)
|
||||
prettyprint
|
||||
prawn (2.5.0)
|
||||
matrix (~> 0.4)
|
||||
pdf-core (~> 0.10.0)
|
||||
ttfunk (~> 1.8)
|
||||
prawn-qrcode (0.5.2)
|
||||
prawn (>= 1)
|
||||
rqrcode (>= 1.0.0)
|
||||
prettyprint (0.2.0)
|
||||
prism (1.4.0)
|
||||
propshaft (1.2.1)
|
||||
@@ -354,6 +362,7 @@ GEM
|
||||
sqlite3 (2.7.3-aarch64-linux-musl)
|
||||
sqlite3 (2.7.3-arm-linux-gnu)
|
||||
sqlite3 (2.7.3-arm-linux-musl)
|
||||
sqlite3 (2.7.3-x86_64-darwin)
|
||||
sqlite3 (2.7.3-x86_64-linux-gnu)
|
||||
sqlite3 (2.7.3-x86_64-linux-musl)
|
||||
sshkit (1.24.0)
|
||||
@@ -370,8 +379,11 @@ GEM
|
||||
thor (1.4.0)
|
||||
thruster (0.1.15)
|
||||
thruster (0.1.15-aarch64-linux)
|
||||
thruster (0.1.15-x86_64-darwin)
|
||||
thruster (0.1.15-x86_64-linux)
|
||||
timeout (0.4.3)
|
||||
ttfunk (1.8.0)
|
||||
bigdecimal (~> 3.1)
|
||||
turbo-rails (2.0.16)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
@@ -404,6 +416,7 @@ PLATFORMS
|
||||
aarch64-linux-musl
|
||||
arm-linux-gnu
|
||||
arm-linux-musl
|
||||
x86_64-darwin-24
|
||||
x86_64-linux-gnu
|
||||
x86_64-linux-musl
|
||||
|
||||
@@ -415,7 +428,6 @@ DEPENDENCIES
|
||||
debug
|
||||
devise (~> 4.9)
|
||||
dotenv-rails
|
||||
grover
|
||||
jbuilder
|
||||
jsbundling-rails
|
||||
kamal
|
||||
@@ -424,6 +436,8 @@ DEPENDENCIES
|
||||
minitest-reporters (~> 1.7)
|
||||
mocha
|
||||
mysql2 (~> 0.5)
|
||||
prawn (~> 2.5)
|
||||
prawn-qrcode (~> 0.5)
|
||||
propshaft
|
||||
puma (>= 5.0)
|
||||
rails (~> 8.0.2, >= 8.0.2.1)
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
# Aperonight Application Refactoring Summary
|
||||
|
||||
## Overview
|
||||
This document summarizes the comprehensive refactoring work performed to ensure all code in the Aperonight application is useful and well-documented.
|
||||
|
||||
## Phase 1: Previous Code Cleanup (Already Completed)
|
||||
|
||||
### Files Removed
|
||||
- **Unused JavaScript Controllers**: shadcn_test_controller.js, featured_event_controller.js, event_form_controller.js, ticket_type_form_controller.js
|
||||
- **Unused React Components**: button.jsx, utils.js
|
||||
- **Duplicate Configuration**: env.example file
|
||||
|
||||
### Dependencies Removed
|
||||
- **Alpine.js Dependencies**: alpinejs, @types/alpinejs (unused in production)
|
||||
|
||||
## Phase 2: Current Refactoring Work
|
||||
|
||||
### 1. Code Cleanup and Unused Code Removal
|
||||
|
||||
#### Removed Dead Code
|
||||
- **TicketsController**: Removed unused `create_stripe_session` method (lines 78-105) that duplicated functionality already present in OrdersController
|
||||
- The legacy TicketsController now properly focuses only on redirects and backward compatibility
|
||||
|
||||
#### Fixed Issues and Improvements
|
||||
- **ApplicationHelper**: Fixed typo in comment ("prince" → "price")
|
||||
- **API Security**: Replaced hardcoded API key with environment variable lookup for better security
|
||||
- **User Validations**: Improved name length validations (2-50 chars instead of restrictive 3-12 chars)
|
||||
|
||||
### 2. Enhanced Documentation and Comments
|
||||
|
||||
#### Models (Now Comprehensively Documented)
|
||||
- **User**: Enhanced comments explaining Devise modules and authorization methods
|
||||
- **Event**: Detailed documentation of state enum, validations, and scopes
|
||||
- **Order**: Comprehensive documentation of lifecycle management and payment processing
|
||||
- **Ticket**: Clear explanation of ticket states and QR code generation
|
||||
- **TicketType**: Documented pricing methods and availability logic
|
||||
|
||||
#### Controllers (Improved Documentation)
|
||||
- **EventsController**: Added detailed method documentation and purpose explanation
|
||||
- **OrdersController**: Already well-documented, verified completeness
|
||||
- **TicketsController**: Enhanced comments explaining legacy redirect functionality
|
||||
- **ApiController**: Improved API authentication documentation with security notes
|
||||
|
||||
#### Services (Enhanced Documentation)
|
||||
- **StripeInvoiceService**: Already excellently documented
|
||||
- **TicketPdfGenerator**: Added class-level documentation and suppressed font warnings
|
||||
|
||||
#### Jobs (Comprehensive Documentation)
|
||||
- **CleanupExpiredDraftsJob**: Added comprehensive documentation and improved error handling
|
||||
- **ExpiredOrdersCleanupJob**: Already well-documented
|
||||
- **StripeInvoiceGenerationJob**: Already well-documented
|
||||
|
||||
#### Helpers (YARD-Style Documentation)
|
||||
- **FlashMessagesHelper**: Added detailed YARD-style documentation with examples
|
||||
- **LucideHelper**: Already well-documented
|
||||
- **StripeHelper**: Verified documentation completeness
|
||||
|
||||
### 3. Code Quality Improvements
|
||||
|
||||
#### Security Enhancements
|
||||
- **ApiController**: Moved API key to environment variables/Rails credentials
|
||||
- Maintained secure authentication patterns throughout
|
||||
|
||||
#### Performance Optimizations
|
||||
- Verified proper use of `includes` for eager loading
|
||||
- Confirmed efficient database queries with scopes
|
||||
- Proper use of `find_each` for batch processing
|
||||
|
||||
#### Error Handling
|
||||
- Enhanced error handling in cleanup jobs
|
||||
- Maintained robust error handling in payment processing
|
||||
- Added graceful fallbacks where appropriate
|
||||
|
||||
### 4. Code Organization and Structure
|
||||
|
||||
#### Structure Verification
|
||||
- Confirmed logical controller organization
|
||||
- Verified proper separation of concerns
|
||||
- Maintained clean service object patterns
|
||||
- Proper use of Rails conventions
|
||||
|
||||
## Files Modified in Current Refactoring
|
||||
|
||||
1. `app/controllers/tickets_controller.rb` - Removed unused method, fixed layout
|
||||
2. `app/controllers/api_controller.rb` - Security improvement, removed hardcoded key
|
||||
3. `app/controllers/events_controller.rb` - Enhanced documentation
|
||||
4. `app/helpers/application_helper.rb` - Fixed typo
|
||||
5. `app/helpers/flash_messages_helper.rb` - Added comprehensive documentation
|
||||
6. `app/jobs/cleanup_expired_drafts_job.rb` - Enhanced documentation and error handling
|
||||
7. `app/models/user.rb` - Improved validations
|
||||
8. `app/services/ticket_pdf_generator.rb` - Added documentation and suppressed warnings
|
||||
|
||||
## Quality Metrics
|
||||
|
||||
- **Tests**: 200 tests, 454 assertions, 0 failures, 0 errors, 0 skips
|
||||
- **RuboCop**: All style issues resolved automatically
|
||||
- **Code Coverage**: Maintained existing coverage
|
||||
- **Documentation**: Significantly improved throughout codebase
|
||||
- **Bundle Size**: No increase, maintenance of efficient build
|
||||
|
||||
## Security Improvements
|
||||
|
||||
1. **API Authentication**: Moved from hardcoded to environment-based API keys
|
||||
2. **Input Validation**: Improved user input validations
|
||||
3. **Error Handling**: Enhanced error messages without exposing sensitive information
|
||||
|
||||
## Recommendations for Future Development
|
||||
|
||||
1. **Environment Variables**: Ensure API_KEY is set in production environment
|
||||
2. **Monitoring**: Consider adding metrics for cleanup job performance
|
||||
3. **Testing**: Add integration tests for the refactored components
|
||||
4. **Documentation**: Maintain the documentation standards established
|
||||
5. **Security**: Regular audit of dependencies and authentication mechanisms
|
||||
|
||||
## Conclusion
|
||||
|
||||
The Aperonight application has been successfully refactored to ensure all code is useful, well-documented, and follows Rails best practices. The codebase is now more maintainable, secure, and provides a better developer experience. All existing functionality is preserved while significantly improving code quality and documentation standards.
|
||||
|
||||
**Total Impact:**
|
||||
- Removed unused code reducing maintenance overhead
|
||||
- Enhanced security with proper credential management
|
||||
- Improved documentation for better maintainability
|
||||
- Maintained 100% test coverage with 0 failures
|
||||
- Preserved all existing functionality
|
||||
185
app/assets/javascripts/qr_generator.js
Normal file
185
app/assets/javascripts/qr_generator.js
Normal file
@@ -0,0 +1,185 @@
|
||||
// Self-contained QR Code Generator
|
||||
// No external dependencies required
|
||||
|
||||
class QRCodeGenerator {
|
||||
constructor() {
|
||||
// QR Code error correction levels
|
||||
this.errorCorrectionLevels = {
|
||||
L: 1, // Low ~7%
|
||||
M: 0, // Medium ~15%
|
||||
Q: 3, // Quartile ~25%
|
||||
H: 2 // High ~30%
|
||||
};
|
||||
|
||||
// Mode indicators
|
||||
this.modes = {
|
||||
NUMERIC: 1,
|
||||
ALPHANUMERIC: 2,
|
||||
BYTE: 4,
|
||||
KANJI: 8
|
||||
};
|
||||
}
|
||||
|
||||
// Generate QR code as SVG
|
||||
generateSVG(text, options = {}) {
|
||||
const size = options.size || 200;
|
||||
const margin = options.margin || 4;
|
||||
const errorCorrection = options.errorCorrection || 'M';
|
||||
|
||||
try {
|
||||
const qrData = this.createQRData(text, errorCorrection);
|
||||
const moduleSize = (size - 2 * margin) / qrData.length;
|
||||
|
||||
let svg = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">`;
|
||||
svg += `<rect width="${size}" height="${size}" fill="white"/>`;
|
||||
|
||||
for (let row = 0; row < qrData.length; row++) {
|
||||
for (let col = 0; col < qrData[row].length; col++) {
|
||||
if (qrData[row][col]) {
|
||||
const x = margin + col * moduleSize;
|
||||
const y = margin + row * moduleSize;
|
||||
svg += `<rect x="${x}" y="${y}" width="${moduleSize}" height="${moduleSize}" fill="black"/>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svg += '</svg>';
|
||||
return svg;
|
||||
} catch (error) {
|
||||
console.error('QR Code generation failed:', error);
|
||||
return this.createErrorSVG(size);
|
||||
}
|
||||
}
|
||||
|
||||
// Create QR code data matrix (simplified implementation)
|
||||
createQRData(text, errorCorrection) {
|
||||
// For simplicity, we'll create a basic QR code pattern
|
||||
// This is a minimal implementation - real QR codes are much more complex
|
||||
|
||||
const version = this.determineVersion(text.length);
|
||||
const size = 21 + (version - 1) * 4; // QR code size formula
|
||||
|
||||
// Initialize matrix
|
||||
const matrix = Array(size).fill().map(() => Array(size).fill(false));
|
||||
|
||||
// Add finder patterns (corners)
|
||||
this.addFinderPatterns(matrix);
|
||||
|
||||
// Add timing patterns
|
||||
this.addTimingPatterns(matrix);
|
||||
|
||||
// Add data (simplified - just create a pattern based on text)
|
||||
this.addDataPattern(matrix, text);
|
||||
|
||||
return matrix;
|
||||
}
|
||||
|
||||
determineVersion(length) {
|
||||
// Simplified version determination
|
||||
if (length <= 25) return 1;
|
||||
if (length <= 47) return 2;
|
||||
if (length <= 77) return 3;
|
||||
return 4; // Max we'll support in this simple implementation
|
||||
}
|
||||
|
||||
addFinderPatterns(matrix) {
|
||||
const size = matrix.length;
|
||||
const pattern = [
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,0,0,0,0,0,1],
|
||||
[1,0,1,1,1,0,1],
|
||||
[1,0,1,1,1,0,1],
|
||||
[1,0,1,1,1,0,1],
|
||||
[1,0,0,0,0,0,1],
|
||||
[1,1,1,1,1,1,1]
|
||||
];
|
||||
|
||||
// Top-left
|
||||
this.placePattern(matrix, 0, 0, pattern);
|
||||
// Top-right
|
||||
this.placePattern(matrix, 0, size - 7, pattern);
|
||||
// Bottom-left
|
||||
this.placePattern(matrix, size - 7, 0, pattern);
|
||||
}
|
||||
|
||||
addTimingPatterns(matrix) {
|
||||
const size = matrix.length;
|
||||
|
||||
// Horizontal timing pattern
|
||||
for (let i = 8; i < size - 8; i++) {
|
||||
matrix[6][i] = i % 2 === 0;
|
||||
}
|
||||
|
||||
// Vertical timing pattern
|
||||
for (let i = 8; i < size - 8; i++) {
|
||||
matrix[i][6] = i % 2 === 0;
|
||||
}
|
||||
}
|
||||
|
||||
addDataPattern(matrix, text) {
|
||||
const size = matrix.length;
|
||||
|
||||
// Simple data pattern based on text hash
|
||||
let hash = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
hash = ((hash << 5) - hash + text.charCodeAt(i)) & 0xffffffff;
|
||||
}
|
||||
|
||||
// Fill available spaces with pattern based on hash
|
||||
for (let row = 0; row < size; row++) {
|
||||
for (let col = 0; col < size; col++) {
|
||||
if (!this.isReserved(row, col, size)) {
|
||||
matrix[row][col] = ((hash >> ((row + col) % 32)) & 1) === 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
placePattern(matrix, startRow, startCol, pattern) {
|
||||
for (let row = 0; row < pattern.length; row++) {
|
||||
for (let col = 0; col < pattern[row].length; col++) {
|
||||
matrix[startRow + row][startCol + col] = pattern[row][col] === 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isReserved(row, col, size) {
|
||||
// Check if position is reserved for finder patterns, timing patterns, etc.
|
||||
|
||||
// Finder patterns
|
||||
if ((row < 9 && col < 9) || // Top-left
|
||||
(row < 9 && col >= size - 8) || // Top-right
|
||||
(row >= size - 8 && col < 9)) { // Bottom-left
|
||||
return true;
|
||||
}
|
||||
|
||||
// Timing patterns
|
||||
if (row === 6 || col === 6) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
createErrorSVG(size) {
|
||||
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="${size}" height="${size}" fill="#f3f4f6"/>
|
||||
<text x="${size/2}" y="${size/2-10}" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#6b7280">QR Code</text>
|
||||
<text x="${size/2}" y="${size/2+10}" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#6b7280">Error</text>
|
||||
</svg>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Global function for easy access
|
||||
window.generateQRCode = function(text, containerId, options = {}) {
|
||||
const generator = new QRCodeGenerator();
|
||||
const container = document.getElementById(containerId);
|
||||
|
||||
if (!container) {
|
||||
console.error('Container not found:', containerId);
|
||||
return;
|
||||
}
|
||||
|
||||
const svg = generator.generateSVG(text, options);
|
||||
container.innerHTML = svg;
|
||||
};
|
||||
@@ -13,16 +13,3 @@
|
||||
|
||||
/* Import pages */
|
||||
@import "pages/home";
|
||||
|
||||
/* QR Code Styles */
|
||||
.qr-code-container {
|
||||
@apply flex items-center justify-center;
|
||||
}
|
||||
|
||||
.qr-code-container svg {
|
||||
max-width: 100% !important;
|
||||
max-height: 100% !important;
|
||||
width: 208px !important;
|
||||
height: 208px !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
816
app/assets/stylesheets/design_system.css
Normal file
816
app/assets/stylesheets/design_system.css
Normal file
@@ -0,0 +1,816 @@
|
||||
/**
|
||||
* Aperonight Design System
|
||||
* Generated from homepage analysis
|
||||
* A modern, professional design system for event platforms
|
||||
*/
|
||||
|
||||
/* === ROOT VARIABLES === */
|
||||
:root {
|
||||
/* Brand Colors */
|
||||
--brand-primary: #667eea;
|
||||
--brand-secondary: #764ba2;
|
||||
--brand-accent: #facc15; /* yellow-400 */
|
||||
--brand-accent-dark: #eab308; /* yellow-500 */
|
||||
|
||||
/* Neutral Colors */
|
||||
--color-white: #ffffff;
|
||||
--color-black: #000000;
|
||||
--color-gray-50: #f9fafb;
|
||||
--color-gray-100: #f3f4f6;
|
||||
--color-gray-200: #e5e7eb;
|
||||
--color-gray-300: #d1d5db;
|
||||
--color-gray-400: #9ca3af;
|
||||
--color-gray-500: #6b7280;
|
||||
--color-gray-600: #4b5563;
|
||||
--color-gray-700: #374151;
|
||||
--color-gray-800: #1f2937;
|
||||
--color-gray-900: #111827;
|
||||
|
||||
/* Purple Shades */
|
||||
--color-purple-600: #9333ea;
|
||||
--color-purple-700: #7c3aed;
|
||||
--color-purple-800: #6b21a8;
|
||||
|
||||
/* Blue Shades */
|
||||
--color-blue-600: #2563eb;
|
||||
--color-blue-700: #1d4ed8;
|
||||
|
||||
/* Typography */
|
||||
--font-family-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
||||
--font-family-mono: ui-monospace, SFMono-Regular, 'SF Mono', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
|
||||
/* Font Sizes */
|
||||
--text-xs: 0.75rem; /* 12px */
|
||||
--text-sm: 0.875rem; /* 14px */
|
||||
--text-base: 1rem; /* 16px */
|
||||
--text-lg: 1.125rem; /* 18px */
|
||||
--text-xl: 1.25rem; /* 20px */
|
||||
--text-2xl: 1.5rem; /* 24px */
|
||||
--text-3xl: 1.875rem; /* 30px */
|
||||
--text-4xl: 2.25rem; /* 36px */
|
||||
--text-5xl: 3rem; /* 48px */
|
||||
--text-6xl: 3.75rem; /* 60px */
|
||||
|
||||
/* Font Weights */
|
||||
--font-medium: 500;
|
||||
--font-semibold: 600;
|
||||
--font-bold: 700;
|
||||
|
||||
/* Spacing Scale */
|
||||
--space-1: 0.25rem; /* 4px */
|
||||
--space-2: 0.5rem; /* 8px */
|
||||
--space-3: 0.75rem; /* 12px */
|
||||
--space-4: 1rem; /* 16px */
|
||||
--space-6: 1.5rem; /* 24px */
|
||||
--space-8: 2rem; /* 32px */
|
||||
--space-12: 3rem; /* 48px */
|
||||
--space-16: 4rem; /* 64px */
|
||||
--space-24: 6rem; /* 96px */
|
||||
|
||||
/* Border Radius */
|
||||
--radius-sm: 0.375rem; /* 6px */
|
||||
--radius-md: 0.5rem; /* 8px */
|
||||
--radius-lg: 0.75rem; /* 12px */
|
||||
--radius-xl: 1rem; /* 16px */
|
||||
--radius-2xl: 1.25rem; /* 20px */
|
||||
--radius-3xl: 1.5rem; /* 24px */
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||
|
||||
/* Gradients */
|
||||
--gradient-primary: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-secondary) 100%);
|
||||
--gradient-overlay: rgba(0, 0, 0, 0.3);
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: all 0.2s ease;
|
||||
--transition-medium: all 0.3s ease;
|
||||
--transition-slow: all 0.5s ease;
|
||||
}
|
||||
|
||||
/* === BASE STYLES === */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
line-height: 1.5;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-family: var(--font-family-sans);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family-sans) !important;
|
||||
line-height: 1.6;
|
||||
color: var(--color-gray-900) !important;
|
||||
background-color: var(--color-white) !important;
|
||||
}
|
||||
|
||||
/* === TYPOGRAPHY SYSTEM === */
|
||||
.text-xs { font-size: var(--text-xs); }
|
||||
.text-sm { font-size: var(--text-sm); }
|
||||
.text-base { font-size: var(--text-base); }
|
||||
.text-lg { font-size: var(--text-lg); }
|
||||
.text-xl { font-size: var(--text-xl); }
|
||||
.text-2xl { font-size: var(--text-2xl); }
|
||||
.text-3xl { font-size: var(--text-3xl); }
|
||||
.text-4xl { font-size: var(--text-4xl); }
|
||||
.text-5xl { font-size: var(--text-5xl); }
|
||||
.text-6xl { font-size: var(--text-6xl); }
|
||||
|
||||
.font-medium { font-weight: var(--font-medium); }
|
||||
.font-semibold { font-weight: var(--font-semibold); }
|
||||
.font-bold { font-weight: var(--font-bold); }
|
||||
|
||||
.leading-tight { line-height: 1.25; }
|
||||
.leading-normal { line-height: 1.5; }
|
||||
.leading-relaxed { line-height: 1.625; }
|
||||
|
||||
/* === BUTTON SYSTEM === */
|
||||
.btn {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-3) var(--space-6);
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-semibold);
|
||||
border-radius: var(--radius-full);
|
||||
transition: var(--transition-fast);
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--color-white) !important;
|
||||
color: var(--color-gray-900) !important;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--color-gray-100) !important;
|
||||
box-shadow: var(--shadow-xl);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: transparent !important;
|
||||
color: var(--color-white) !important;
|
||||
border: 2px solid var(--color-white) !important;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: var(--color-white) !important;
|
||||
color: var(--color-gray-900) !important;
|
||||
}
|
||||
|
||||
.btn-secondary-alt {
|
||||
background-color: transparent !important;
|
||||
color: var(--color-gray-700) !important;
|
||||
border: 2px solid var(--color-gray-300) !important;
|
||||
}
|
||||
|
||||
.btn-secondary-alt:hover {
|
||||
background-color: var(--color-gray-100) !important;
|
||||
color: var(--color-gray-900) !important;
|
||||
border-color: var(--color-gray-400) !important;
|
||||
}
|
||||
|
||||
.btn-accent {
|
||||
background-color: var(--color-purple-600) !important;
|
||||
color: var(--color-white) !important;
|
||||
}
|
||||
|
||||
.btn-accent:hover {
|
||||
background-color: var(--color-purple-700) !important;
|
||||
}
|
||||
|
||||
.btn-dark {
|
||||
background-color: var(--color-gray-900) !important;
|
||||
color: var(--color-white) !important;
|
||||
}
|
||||
|
||||
.btn-dark:hover {
|
||||
background-color: var(--color-gray-800) !important;
|
||||
}
|
||||
|
||||
/* Button Sizes */
|
||||
.btn-sm {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: var(--space-4) var(--space-8);
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
/* === CARD SYSTEM === */
|
||||
.card {
|
||||
background-color: var(--color-white) !important;
|
||||
border-radius: var(--radius-2xl) !important;
|
||||
box-shadow: var(--shadow-sm);
|
||||
overflow: hidden;
|
||||
transition: var(--transition-medium);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-event {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-event-image {
|
||||
aspect-ratio: 4/3;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-2xl);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-event-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: var(--transition-medium);
|
||||
}
|
||||
|
||||
.card-event:hover .card-event-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.card-event-badge {
|
||||
position: absolute;
|
||||
top: var(--space-4);
|
||||
left: var(--space-4);
|
||||
background-color: var(--brand-accent) !important;
|
||||
color: var(--color-gray-900) !important;
|
||||
padding: var(--space-1) var(--space-3);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.card-event-price {
|
||||
position: absolute;
|
||||
bottom: var(--space-4);
|
||||
right: var(--space-4);
|
||||
background-color: rgba(255, 255, 255, 0.9) !important;
|
||||
backdrop-filter: blur(4px);
|
||||
color: var(--color-gray-900) !important;
|
||||
padding: var(--space-1) var(--space-3);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-bold);
|
||||
}
|
||||
|
||||
.card-event-content {
|
||||
padding: var(--space-6);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-event-title {
|
||||
font-size: var(--text-2xl) !important;
|
||||
font-weight: var(--font-bold) !important;
|
||||
color: var(--color-gray-900) !important;
|
||||
margin-bottom: var(--space-2);
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
|
||||
.card-event:hover .card-event-title {
|
||||
color: var(--color-purple-600) !important;
|
||||
}
|
||||
|
||||
.card-event-meta {
|
||||
color: var(--color-gray-600) !important;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.card-event-description {
|
||||
color: var(--color-gray-500) !important;
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--leading-relaxed);
|
||||
max-width: 20rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* === HERO SYSTEM === */
|
||||
.hero {
|
||||
background: var(--gradient-primary) !important;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--gradient-overlay);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
color: var(--color-white) !important;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: var(--text-4xl) !important;
|
||||
font-weight: var(--font-bold) !important;
|
||||
line-height: var(--leading-tight);
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: var(--text-xl) !important;
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
margin-bottom: var(--space-8);
|
||||
max-width: 32rem;
|
||||
}
|
||||
|
||||
.hero-accent {
|
||||
color: var(--brand-accent) !important;
|
||||
}
|
||||
|
||||
/* Responsive Hero */
|
||||
@media (min-width: 1024px) {
|
||||
.hero-title {
|
||||
font-size: var(--text-6xl) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* === METRICS SYSTEM === */
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--space-8);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.metrics-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
transition: var(--transition-medium);
|
||||
}
|
||||
|
||||
.metric-number {
|
||||
font-size: var(--text-4xl) !important;
|
||||
font-weight: var(--font-bold) !important;
|
||||
color: var(--color-purple-600) !important;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.metric-number {
|
||||
font-size: var(--text-5xl) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: var(--color-gray-600) !important;
|
||||
font-weight: var(--font-medium) !important;
|
||||
}
|
||||
|
||||
/* === SECTION SYSTEM === */
|
||||
.section {
|
||||
padding: var(--space-16) 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--space-12);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: var(--text-3xl) !important;
|
||||
font-weight: var(--font-bold) !important;
|
||||
color: var(--color-gray-900) !important;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.section-title {
|
||||
font-size: var(--text-4xl) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.section-description {
|
||||
font-size: var(--text-xl) !important;
|
||||
color: var(--color-gray-600) !important;
|
||||
max-width: 40rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* === GRID SYSTEM === */
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: var(--space-8);
|
||||
}
|
||||
|
||||
.grid-1 { grid-template-columns: 1fr; }
|
||||
.grid-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.grid-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.grid-md-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.grid-md-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.grid-lg-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.grid-lg-4 { grid-template-columns: repeat(4, 1fr); }
|
||||
}
|
||||
|
||||
/* === UTILITY CLASSES === */
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding-left: var(--space-4);
|
||||
padding-right: var(--space-4);
|
||||
}
|
||||
|
||||
.text-center { text-align: center; }
|
||||
.text-left { text-align: left; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
.bg-white { background-color: var(--color-white) !important; }
|
||||
.bg-gray-50 { background-color: var(--color-gray-50) !important; }
|
||||
.bg-gray-900 { background-color: var(--color-gray-900) !important; }
|
||||
|
||||
.text-white { color: var(--color-white) !important; }
|
||||
.text-gray-600 { color: var(--color-gray-600) !important; }
|
||||
.text-gray-900 { color: var(--color-gray-900) !important; }
|
||||
|
||||
.rounded-full { border-radius: var(--radius-full) !important; }
|
||||
.rounded-2xl { border-radius: var(--radius-2xl) !important; }
|
||||
|
||||
.shadow-lg { box-shadow: var(--shadow-lg) !important; }
|
||||
.shadow-xl { box-shadow: var(--shadow-xl) !important; }
|
||||
|
||||
.mb-2 { margin-bottom: var(--space-2) !important; }
|
||||
.mb-4 { margin-bottom: var(--space-4) !important; }
|
||||
.mb-6 { margin-bottom: var(--space-6) !important; }
|
||||
.mb-8 { margin-bottom: var(--space-8) !important; }
|
||||
.mb-12 { margin-bottom: var(--space-12) !important; }
|
||||
|
||||
.p-4 { padding: var(--space-4) !important; }
|
||||
.p-6 { padding: var(--space-6) !important; }
|
||||
.p-8 { padding: var(--space-8) !important; }
|
||||
|
||||
.flex { display: flex !important; }
|
||||
.items-center { align-items: center; }
|
||||
.justify-center { justify-content: center; }
|
||||
.gap-4 { gap: var(--space-4); }
|
||||
|
||||
.transition { transition: var(--transition-fast); }
|
||||
|
||||
.max-w-lg { max-width: 32rem; }
|
||||
.max-w-2xl { max-width: 42rem; }
|
||||
.max-w-4xl { max-width: 56rem; }
|
||||
|
||||
/* === BREADCRUMB SYSTEM === */
|
||||
.breadcrumb {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
background-color: var(--color-white) !important;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.breadcrumb-item a {
|
||||
color: var(--color-gray-700) !important;
|
||||
text-decoration: none;
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
|
||||
.breadcrumb-item a:hover {
|
||||
color: var(--color-purple-600) !important;
|
||||
}
|
||||
|
||||
.breadcrumb-item:not(:last-child)::after {
|
||||
content: '';
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-left: var(--space-2);
|
||||
background: url("data:image/svg+xml,%3csvg fill='%234b5563' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill-rule='evenodd' d='M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z' clip-rule='evenodd'/%3e%3c/svg%3e") center no-repeat;
|
||||
background-size: 1rem;
|
||||
}
|
||||
|
||||
.breadcrumb-current {
|
||||
color: var(--color-purple-600) !important;
|
||||
}
|
||||
|
||||
/* === PAGE HEADER SYSTEM === */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: var(--space-8) 0;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--text-3xl) !important;
|
||||
font-weight: var(--font-bold) !important;
|
||||
color: var(--color-gray-900) !important;
|
||||
}
|
||||
|
||||
.page-meta {
|
||||
font-size: var(--text-sm) !important;
|
||||
color: var(--color-gray-500) !important;
|
||||
}
|
||||
|
||||
/* === EVENTS GRID SYSTEM === */
|
||||
.events-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-6);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.events-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.events-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.event-card {
|
||||
background-color: var(--color-white) !important;
|
||||
border-radius: var(--radius-xl) !important;
|
||||
box-shadow: var(--shadow-md);
|
||||
overflow: hidden;
|
||||
transition: var(--transition-medium);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.event-card:hover {
|
||||
box-shadow: var(--shadow-xl);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.event-card-image {
|
||||
height: 12rem;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.event-card-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: var(--transition-medium);
|
||||
}
|
||||
|
||||
.event-card:hover .event-card-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.event-card-placeholder {
|
||||
height: 12rem;
|
||||
background: var(--gradient-primary) !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.event-card-placeholder svg {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
}
|
||||
|
||||
.event-card-content {
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.event-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.event-card-title {
|
||||
font-size: var(--text-xl) !important;
|
||||
font-weight: var(--font-bold) !important;
|
||||
color: var(--color-gray-900) !important;
|
||||
margin-bottom: var(--space-1);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.event-card-venue {
|
||||
font-size: var(--text-xs) !important;
|
||||
color: var(--color-gray-500) !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.event-card-date {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: var(--space-2) calc(var(--space-2) + var(--space-1));
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-xs) !important;
|
||||
font-weight: var(--font-medium) !important;
|
||||
background-color: rgba(147, 51, 234, 0.1) !important;
|
||||
color: var(--color-purple-800) !important;
|
||||
white-space: nowrap;
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.event-card-description {
|
||||
color: var(--color-gray-600) !important;
|
||||
font-size: var(--text-sm) !important;
|
||||
line-height: 1.4;
|
||||
margin-bottom: var(--space-4);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.event-card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.event-card-price {
|
||||
font-size: var(--text-sm) !important;
|
||||
font-weight: var(--font-medium) !important;
|
||||
color: var(--color-gray-900) !important;
|
||||
}
|
||||
|
||||
.event-card-price-unavailable {
|
||||
font-size: var(--text-sm) !important;
|
||||
color: var(--color-gray-500) !important;
|
||||
}
|
||||
|
||||
.event-card-link {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border: 1px solid transparent;
|
||||
font-size: var(--text-sm) !important;
|
||||
font-weight: var(--font-medium) !important;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
color: var(--color-white) !important;
|
||||
background: var(--gradient-primary) !important;
|
||||
text-decoration: none !important;
|
||||
transition: var(--transition-fast);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.event-card-link:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* === EMPTY STATE SYSTEM === */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: var(--space-16) var(--space-4);
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
margin: 0 auto var(--space-6);
|
||||
background: linear-gradient(135deg, rgba(147, 51, 234, 0.1) 0%, rgba(79, 70, 229, 0.1) 100%) !important;
|
||||
border-radius: var(--radius-full);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.empty-state-icon svg {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
color: var(--color-purple-600) !important;
|
||||
}
|
||||
|
||||
.empty-state-title {
|
||||
font-size: var(--text-lg) !important;
|
||||
font-weight: var(--font-medium) !important;
|
||||
color: var(--color-gray-900) !important;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.empty-state-description {
|
||||
color: var(--color-gray-500) !important;
|
||||
margin-bottom: var(--space-6);
|
||||
max-width: 24rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* === PAGINATION SYSTEM === */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: var(--space-8);
|
||||
}
|
||||
|
||||
.pagination .page-item {
|
||||
margin: 0 var(--space-1);
|
||||
}
|
||||
|
||||
.pagination .page-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--text-sm) !important;
|
||||
font-weight: var(--font-medium) !important;
|
||||
color: var(--color-gray-600) !important;
|
||||
background-color: var(--color-white) !important;
|
||||
border: 1px solid var(--color-gray-200) !important;
|
||||
border-radius: var(--radius-md);
|
||||
text-decoration: none !important;
|
||||
transition: var(--transition-fast);
|
||||
min-width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.pagination .page-link:hover {
|
||||
background-color: var(--color-gray-50) !important;
|
||||
border-color: var(--color-purple-300) !important;
|
||||
color: var(--color-purple-600) !important;
|
||||
}
|
||||
|
||||
.pagination .page-item.active .page-link {
|
||||
background-color: var(--color-purple-600) !important;
|
||||
border-color: var(--color-purple-600) !important;
|
||||
color: var(--color-white) !important;
|
||||
}
|
||||
|
||||
.pagination .page-item.disabled .page-link {
|
||||
color: var(--color-gray-300) !important;
|
||||
background-color: var(--color-white) !important;
|
||||
border-color: var(--color-gray-200) !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* === RESPONSIVE UTILITIES === */
|
||||
@media (max-width: 640px) {
|
||||
.sm\:flex-col { flex-direction: column; }
|
||||
.sm\:text-center { text-align: center; }
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--text-2xl) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:flex-row { flex-direction: row; }
|
||||
.sm\:flex-1 { flex: 1; }
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.lg\:justify-start { justify-content: flex-start; }
|
||||
.lg\:text-left { text-align: left; }
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
/* PDF Styles for Ticket Generation */
|
||||
|
||||
body {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
color: #000000;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.ticket-container {
|
||||
max-width: 350px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #2D1B69;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Event name */
|
||||
.event-name {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.event-name h2 {
|
||||
color: #000000;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Ticket info box */
|
||||
.ticket-info-box {
|
||||
background-color: #F9FAFB;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: bold;
|
||||
color: #000000;
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
display: inline-block;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
/* Venue information */
|
||||
.venue-info {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.venue-info h3 {
|
||||
color: #374151;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.venue-details {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.venue-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.venue-address {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
/* QR Code */
|
||||
.qr-code-section {
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.qr-code-section h3 {
|
||||
color: #000000;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.qr-code-container {
|
||||
text-align: center;
|
||||
margin: 0 auto 10px auto;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.qr-code-text {
|
||||
font-size: 8px;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
border-top: 1px solid #E5E7EB;
|
||||
padding-top: 15px;
|
||||
text-align: center;
|
||||
font-size: 8px;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.footer p {
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
.generated-date {
|
||||
margin-top: 5px;
|
||||
}
|
||||
@@ -5,22 +5,33 @@
|
||||
--color-primary-200: #ddd6fe;
|
||||
--color-primary-300: #c4b5fd;
|
||||
--color-primary-400: #a78bfa;
|
||||
--color-primary-500: #8b5cf6;
|
||||
--color-primary-600: #7c3aed;
|
||||
--color-primary-700: #6d28d9;
|
||||
--color-primary-800: #5b21b6;
|
||||
--color-primary-900: #4c1d95;
|
||||
--color-primary-500: #667eea;
|
||||
--color-primary-600: #667eea;
|
||||
--color-primary-700: #5a6fd8;
|
||||
--color-primary-800: #4e63c6;
|
||||
--color-primary-900: #4257b4;
|
||||
|
||||
--color-accent-50: #fdf2f8;
|
||||
--color-accent-100: #fce7f3;
|
||||
--color-accent-200: #fbcfe8;
|
||||
--color-accent-300: #f9a8d4;
|
||||
--color-accent-400: #f472b6;
|
||||
--color-accent-500: #ec4899;
|
||||
--color-accent-600: #db2777;
|
||||
--color-accent-700: #be185d;
|
||||
--color-accent-800: #9d174d;
|
||||
--color-accent-900: #831843;
|
||||
--color-accent-50: #fffbeb;
|
||||
--color-accent-100: #fef3c7;
|
||||
--color-accent-200: #fde68a;
|
||||
--color-accent-300: #fcd34d;
|
||||
--color-accent-400: #facc15;
|
||||
--color-accent-500: #facc15;
|
||||
--color-accent-600: #e6c213;
|
||||
--color-accent-700: #d1b811;
|
||||
--color-accent-800: #bdae0f;
|
||||
--color-accent-900: #a8a40d;
|
||||
|
||||
--color-secondary-50: #f0e9f9;
|
||||
--color-secondary-100: #e2d4f3;
|
||||
--color-secondary-200: #c5a9e7;
|
||||
--color-secondary-300: #a87edc;
|
||||
--color-secondary-400: #8b53d0;
|
||||
--color-secondary-500: #764ba2;
|
||||
--color-secondary-600: #764ba2;
|
||||
--color-secondary-700: #68428f;
|
||||
--color-secondary-800: #5a397c;
|
||||
--color-secondary-900: #4c3069;
|
||||
|
||||
--color-neutral-50: #fafafa;
|
||||
--color-neutral-100: #f5f5f5;
|
||||
@@ -87,9 +98,9 @@
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
|
||||
--shadow-purple-sm: 0 1px 3px 0 rgba(168, 85, 247, 0.1), 0 1px 2px 0 rgba(168, 85, 247, 0.06);
|
||||
--shadow-purple-md: 0 4px 6px -1px rgba(168, 85, 247, 0.1), 0 2px 4px -1px rgba(168, 85, 247, 0.06);
|
||||
--shadow-purple-lg: 0 10px 15px -3px rgba(168, 85, 247, 0.1), 0 4px 6px -2px rgba(168, 85, 247, 0.05);
|
||||
--shadow-purple-sm: 0 1px 3px 0 rgba(102, 126, 234, 0.1), 0 1px 2px 0 rgba(102, 126, 234, 0.06);
|
||||
--shadow-purple-md: 0 4px 6px -1px rgba(102, 126, 234, 0.1), 0 2px 4px -1px rgba(102, 126, 234, 0.06);
|
||||
--shadow-purple-lg: 0 10px 15px -3px rgba(102, 126, 234, 0.1), 0 4px 6px -2px rgba(102, 126, 234, 0.05);
|
||||
|
||||
/* Transitions */
|
||||
--duration-fast: 150ms;
|
||||
@@ -158,7 +169,6 @@ p {
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-lg);
|
||||
transition: all var(--duration-normal) var(--ease-out);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -179,12 +189,13 @@ p {
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-accent-500) 100%);
|
||||
background: var(--color-primary-500);
|
||||
color: white;
|
||||
box-shadow: var(--shadow-purple-md);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--color-primary-600);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-purple-lg);
|
||||
}
|
||||
@@ -199,10 +210,44 @@ p {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-secondary-alt {
|
||||
background-color: transparent;
|
||||
color: var(--color-gray-700);
|
||||
border: 2px solid var(--color-gray-300);
|
||||
}
|
||||
|
||||
.btn-secondary-alt:hover {
|
||||
background-color: var(--color-gray-100);
|
||||
color: var(--color-gray-900);
|
||||
border-color: var(--color-gray-400);
|
||||
}
|
||||
|
||||
.btn-accent {
|
||||
background: var(--color-accent-400);
|
||||
color: var(--color-neutral-900);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.btn-accent:hover {
|
||||
background: var(--color-accent-500);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.btn-dark {
|
||||
background: var(--color-neutral-900);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-dark:hover {
|
||||
background: var(--color-neutral-800);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 2px solid var(--color-primary-600);
|
||||
color: var(--color-primary-600);
|
||||
border: 2px solid var(--color-primary-500);
|
||||
color: var(--color-primary-500);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
@@ -256,7 +301,7 @@ p {
|
||||
outline: none;
|
||||
border-color: var(--color-primary-500);
|
||||
background: white;
|
||||
box-shadow: 0 0 0 4px rgba(168, 85, 247, 0.1);
|
||||
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
@@ -295,7 +340,7 @@ p {
|
||||
outline: none;
|
||||
border-color: var(--color-primary-500);
|
||||
background: white;
|
||||
box-shadow: 0 0 0 4px rgba(168, 85, 247, 0.1);
|
||||
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
@@ -325,7 +370,7 @@ p {
|
||||
.badge-sold-out {
|
||||
background: var(--color-danger-light);
|
||||
color: var(--color-danger-dark);
|
||||
border: 1px solid var(--color-danger);
|
||||
border: 1px border var(--color-danger);
|
||||
}
|
||||
|
||||
.badge-featured {
|
||||
@@ -508,7 +553,7 @@ p {
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, var(--color-primary-500) 0%, var(--color-accent-400) 100%);
|
||||
background: var(--color-primary-500);
|
||||
border-radius: var(--radius-full);
|
||||
transition: width var(--duration-slow) var(--ease-out);
|
||||
}
|
||||
@@ -689,59 +734,216 @@ p {
|
||||
|
||||
/* Breadcrumbs */
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--text-sm);
|
||||
background: white;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--color-neutral-100);
|
||||
margin-bottom: var(--space-8);
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
color: var(--color-neutral-600);
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.breadcrumb-item:hover {
|
||||
.breadcrumb-link {
|
||||
color: var(--color-neutral-700);
|
||||
text-decoration: none;
|
||||
transition: all var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
.breadcrumb-link:hover {
|
||||
color: var(--color-primary-600);
|
||||
}
|
||||
|
||||
.breadcrumb-item.current {
|
||||
color: var(--color-neutral-900);
|
||||
font-weight: 600;
|
||||
.breadcrumb-current {
|
||||
color: var(--color-primary-600);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
color: var(--color-neutral-400);
|
||||
width: var(--space-4);
|
||||
height: var(--space-4);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 var(--space-3);
|
||||
}
|
||||
/* Hero section */
|
||||
.hero {
|
||||
background: linear-gradient(135deg, var(--color-primary-500) 0%, var(--color-secondary-500) 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h1 {
|
||||
.hero::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
color: white;
|
||||
padding: var(--space-16) 0;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: var(--text-5xl);
|
||||
font-weight: 900;
|
||||
line-height: 1.1;
|
||||
margin-bottom: var(--space-4);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-accent {
|
||||
color: var(--color-accent-400);
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
margin-bottom: var(--space-8);
|
||||
text-align: center;
|
||||
max-width: 36rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* Metrics grid */
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: var(--space-6);
|
||||
margin: var(--space-8) 0;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
text-align: center;
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.metric-number {
|
||||
font-size: var(--text-3xl);
|
||||
}
|
||||
font-weight: 800;
|
||||
color: var(--color-primary-600);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: var(--space-3) var(--space-6);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
.btn-md {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
.metric-label {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
font-weight: 600;
|
||||
color: var(--color-neutral-600);
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
padding: var(--space-3);
|
||||
}
|
||||
/* Cards */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-6);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all var(--duration-slow) var(--ease-out);
|
||||
}
|
||||
|
||||
.card.hover-lift:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: var(--shadow-2xl);
|
||||
border-color: var(--color-primary-200);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.card-event {
|
||||
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;
|
||||
}
|
||||
|
||||
.card-event.hover-glow:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: var(--shadow-2xl);
|
||||
border-color: var(--color-primary-200);
|
||||
}
|
||||
|
||||
.card-event-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 4/3;
|
||||
}
|
||||
|
||||
.card-event-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.card-event-badge {
|
||||
position: absolute;
|
||||
top: var(--space-4);
|
||||
left: var(--space-4);
|
||||
background: var(--color-accent-400);
|
||||
color: var(--color-neutral-900);
|
||||
padding: var(--space-1) var(--space-3);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 700;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.card-event-price {
|
||||
position: absolute;
|
||||
bottom: var(--space-4);
|
||||
right: var(--space-4);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(4px);
|
||||
color: var(--color-neutral-900);
|
||||
padding: var(--space-1) var(--space-3);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 700;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.card-event-content {
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.card-event-title {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
color: var(--color-neutral-900);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.card-event-meta {
|
||||
color: var(--color-neutral-600);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.card-event-description {
|
||||
color: var(--color-neutral-500);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Additional styles for enhanced Aperonight design */
|
||||
@@ -788,3 +990,33 @@ p {
|
||||
color: var(--color-neutral-400);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 var(--space-3);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--text-3xl);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: var(--space-3) var(--space-6);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
.btn-md {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
padding: var(--space-3);
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,15 @@
|
||||
module Api
|
||||
module V1
|
||||
class OrdersController < ApiController
|
||||
before_action :authenticate_user!
|
||||
# Skip API key authentication for store_cart action (used by frontend forms)
|
||||
skip_before_action :authenticate_api_key, only: [ :store_cart ]
|
||||
|
||||
before_action :set_order, only: [ :show, :checkout, :retry_payment, :increment_payment_attempt ]
|
||||
before_action :set_event, only: [ :new, :create ]
|
||||
|
||||
# Skip API key authentication for increment_payment_attempt action (used by frontend forms)
|
||||
skip_before_action :authenticate_api_key, only: [ :increment_payment_attempt ]
|
||||
|
||||
# GET /api/v1/orders/new
|
||||
# Returns data needed for new order form
|
||||
def new
|
||||
|
||||
@@ -5,6 +5,9 @@ class ApplicationController < ActionController::Base
|
||||
# Ensures that all non-GET requests include a valid authenticity token
|
||||
protect_from_forgery with: :exception
|
||||
|
||||
# Redirect authenticated users to onboarding if not completed
|
||||
before_action :require_onboarding_completion
|
||||
|
||||
# Restrict access to modern browsers only
|
||||
# Requires browsers to support modern web standards:
|
||||
# - WebP images for better compression
|
||||
@@ -14,4 +17,29 @@ class ApplicationController < ActionController::Base
|
||||
# - CSS nesting and :has() pseudo-class
|
||||
# allow_browser versions: :modern
|
||||
# allow_browser versions: { safari: 16.4, firefox: 121, ie: false }
|
||||
|
||||
private
|
||||
|
||||
def require_onboarding_completion
|
||||
# Skip onboarding check for these paths
|
||||
return if skip_onboarding_check?
|
||||
|
||||
# Only apply to signed-in users
|
||||
if user_signed_in? && current_user.needs_onboarding?
|
||||
redirect_to onboarding_path unless request.path == onboarding_path
|
||||
end
|
||||
end
|
||||
|
||||
def skip_onboarding_check?
|
||||
# Skip for devise controllers (login, signup, password reset, etc.)
|
||||
devise_controller? ||
|
||||
# Skip for onboarding controller itself
|
||||
controller_name == "onboarding" ||
|
||||
# Skip for API endpoints
|
||||
controller_name.start_with?("api/") ||
|
||||
# Skip for health checks
|
||||
controller_name == "rails/health" ||
|
||||
# Skip for home page (when not signed in)
|
||||
(controller_name == "pages" && action_name == "home")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,7 +47,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||
|
||||
# If you have extra params to permit, append them to the sanitizer.
|
||||
def configure_account_update_params
|
||||
devise_parameter_sanitizer.permit(:account_update, keys: [ :last_name, :first_name ])
|
||||
devise_parameter_sanitizer.permit(:account_update, keys: [ :last_name, :first_name, :is_professionnal ])
|
||||
end
|
||||
|
||||
# The path used after sign up.
|
||||
|
||||
38
app/controllers/onboarding_controller.rb
Normal file
38
app/controllers/onboarding_controller.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
class OnboardingController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :redirect_if_onboarding_complete, except: [ :complete ]
|
||||
|
||||
def index
|
||||
# Display the onboarding form
|
||||
end
|
||||
|
||||
def complete
|
||||
if onboarding_params_valid?
|
||||
current_user.update!(onboarding_params)
|
||||
current_user.complete_onboarding!
|
||||
|
||||
flash[:notice] = "Bienvenue sur #{Rails.application.config.app_name} ! Votre profil a été configuré avec succès."
|
||||
redirect_to dashboard_path
|
||||
else
|
||||
flash.now[:alert] = "Veuillez remplir tous les champs requis."
|
||||
render :index
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def onboarding_params
|
||||
params.require(:user).permit(:first_name, :last_name)
|
||||
end
|
||||
|
||||
def onboarding_params_valid?
|
||||
onboarding_params[:first_name].present? &&
|
||||
onboarding_params[:last_name].present?
|
||||
end
|
||||
|
||||
def redirect_if_onboarding_complete
|
||||
if current_user&.onboarding_completed?
|
||||
redirect_to dashboard_path
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,7 +4,7 @@
|
||||
# Orders group multiple tickets together for better transaction management
|
||||
class OrdersController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_order, only: [ :show, :checkout, :retry_payment, :increment_payment_attempt ]
|
||||
before_action :set_order, only: [ :show, :checkout, :retry_payment, :increment_payment_attempt, :invoice ]
|
||||
before_action :set_event, only: [ :new, :create ]
|
||||
|
||||
# Display new order form with name collection
|
||||
@@ -97,9 +97,15 @@ class OrdersController < ApplicationController
|
||||
redirect_to event_order_new_path(@event.slug, @event.id)
|
||||
end
|
||||
|
||||
# Display all user orders
|
||||
def index
|
||||
@orders = current_user.orders.includes(:event, tickets: :ticket_type)
|
||||
.where(status: [ "paid", "completed" ])
|
||||
.order(created_at: :desc)
|
||||
.page(params[:page])
|
||||
end
|
||||
|
||||
# Display order summary
|
||||
#
|
||||
#
|
||||
def show
|
||||
@tickets = @order.tickets.includes(:ticket_type)
|
||||
end
|
||||
@@ -146,9 +152,34 @@ class OrdersController < ApplicationController
|
||||
return
|
||||
end
|
||||
|
||||
# For POST requests, increment the payment attempt counter
|
||||
if request.post?
|
||||
@order.increment_payment_attempt!
|
||||
end
|
||||
|
||||
redirect_to checkout_order_path(@order)
|
||||
end
|
||||
|
||||
# Display invoice for an order
|
||||
def invoice
|
||||
unless @order.status == "paid" || @order.status == "completed"
|
||||
redirect_to order_path(@order), alert: "La facture n'est disponible qu'après le paiement de la commande"
|
||||
return
|
||||
end
|
||||
|
||||
@tickets = @order.tickets.includes(:ticket_type)
|
||||
|
||||
# Get the Stripe invoice if it exists
|
||||
begin
|
||||
@stripe_invoice_id = @order.create_stripe_invoice!
|
||||
@stripe_invoice_pdf_url = @order.stripe_invoice_pdf_url if @stripe_invoice_id
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to retrieve or create Stripe invoice for order #{@order.id}: #{e.message}"
|
||||
@stripe_invoice_id = nil
|
||||
@stripe_invoice_pdf_url = nil
|
||||
end
|
||||
end
|
||||
|
||||
# Handle successful payment
|
||||
def payment_success
|
||||
session_id = params[:session_id]
|
||||
@@ -188,15 +219,8 @@ class OrdersController < ApplicationController
|
||||
# Don't fail the payment process due to job scheduling issues
|
||||
end
|
||||
|
||||
# Send confirmation emails
|
||||
@order.tickets.each do |ticket|
|
||||
begin
|
||||
TicketMailer.purchase_confirmation(ticket).deliver_now
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to send confirmation email for ticket #{ticket.id}: #{e.message}"
|
||||
# Don't fail the entire payment process due to email/PDF generation issues
|
||||
end
|
||||
end
|
||||
# Email confirmation is handled by the order model's mark_as_paid! method
|
||||
# to avoid duplicate emails
|
||||
|
||||
# Clear session data
|
||||
session.delete(:pending_cart)
|
||||
@@ -269,6 +293,19 @@ class OrdersController < ApplicationController
|
||||
}
|
||||
end
|
||||
|
||||
# Add service fee as a separate line item
|
||||
line_items << {
|
||||
price_data: {
|
||||
currency: "eur",
|
||||
product_data: {
|
||||
name: "Frais de service",
|
||||
description: "Frais de traitement de la commande"
|
||||
},
|
||||
unit_amount: 100 # 1€ in cents
|
||||
},
|
||||
quantity: 1
|
||||
}
|
||||
|
||||
Stripe::Checkout::Session.create(
|
||||
payment_method_types: [ "card" ],
|
||||
line_items: line_items,
|
||||
|
||||
@@ -3,44 +3,98 @@
|
||||
class PagesController < ApplicationController
|
||||
before_action :authenticate_user!, only: [ :dashboard ]
|
||||
|
||||
# Homepage showing featured events
|
||||
# Homepage showing featured events as landing page
|
||||
#
|
||||
# Display homepage with featured events and incoming ones
|
||||
# Display homepage with featured events and site metrics for all users
|
||||
def home
|
||||
@featured_events = Event.published.featured.limit(3)
|
||||
# Featured events for the main grid (6-9 events like Shotgun)
|
||||
@featured_events = Event.published.featured.includes(:ticket_types).limit(9)
|
||||
|
||||
if user_signed_in?
|
||||
redirect_to(dashboard_path)
|
||||
# If no featured events, show latest published events
|
||||
if @featured_events.empty?
|
||||
@featured_events = Event.published.includes(:ticket_types).order(created_at: :desc).limit(9)
|
||||
end
|
||||
|
||||
# Upcoming events for additional content
|
||||
@upcoming_events = Event.published.upcoming.limit(6)
|
||||
|
||||
# Site metrics for landing page (with realistic fake data for demo)
|
||||
@total_events = [ Event.published.count, 50 ].max # At least 50 events for demo
|
||||
@total_users = [ User.count, 2500 ].max # At least 2500 users for demo
|
||||
@events_this_month = [ Event.published.where(created_at: 1.month.ago..Time.current).count, 12 ].max # At least 12 this month
|
||||
@active_cities = 5 # Fixed number for demo
|
||||
end
|
||||
|
||||
# User dashboard showing personalized content
|
||||
# Accessible only to authenticated users
|
||||
def dashboard
|
||||
# Metrics for dashboard cards
|
||||
@booked_events = current_user.orders.joins(tickets: { ticket_type: :event })
|
||||
.where(events: { state: :published })
|
||||
.where(orders: { status: [ "paid", "completed" ] })
|
||||
.sum("1")
|
||||
@events_today = Event.published.where("DATE(start_time) = ?", Date.current).count
|
||||
@events_tomorrow = Event.published.where("DATE(start_time) = ?", Date.current + 1).count
|
||||
@upcoming_events = Event.published.upcoming.count
|
||||
|
||||
# User's booked events
|
||||
@user_booked_events = Event.joins(ticket_types: { tickets: :order })
|
||||
.where(orders: { user: current_user }, tickets: { status: "active" })
|
||||
.distinct
|
||||
.limit(5)
|
||||
# User's orders with associated data
|
||||
@user_orders = current_user.orders.includes(:event, tickets: :ticket_type)
|
||||
.where(status: [ "paid", "completed" ])
|
||||
.order(created_at: :desc)
|
||||
.limit(10)
|
||||
|
||||
# Draft orders that can be retried
|
||||
@draft_orders = current_user.orders.includes(tickets: [ :ticket_type, :event ])
|
||||
.can_retry_payment
|
||||
.order(:expires_at)
|
||||
|
||||
# Events sections
|
||||
@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])
|
||||
# Promoter-specific data if user is a promoter
|
||||
if current_user.promoter?
|
||||
@promoter_events = current_user.events.includes(:orders, :tickets)
|
||||
.order(created_at: :desc)
|
||||
.limit(5)
|
||||
|
||||
# Revenue metrics for promoter
|
||||
@total_revenue = current_user.events
|
||||
.joins(:orders)
|
||||
.where(orders: { status: [ "paid", "completed" ] })
|
||||
.sum("orders.total_amount_cents") / 100.0
|
||||
|
||||
@total_tickets_sold = current_user.events
|
||||
.joins(:tickets)
|
||||
.where(tickets: { status: "active" })
|
||||
.count
|
||||
|
||||
@active_events_count = current_user.events.where(state: "published").count
|
||||
@draft_events_count = current_user.events.where(state: "draft").count
|
||||
|
||||
# Recent orders for promoter events
|
||||
@recent_orders = Order.joins(:event)
|
||||
.where(events: { user: current_user })
|
||||
.where(status: [ "paid", "completed" ])
|
||||
.includes(:event, :user, tickets: :ticket_type)
|
||||
.order(created_at: :desc)
|
||||
.limit(10)
|
||||
|
||||
# Monthly revenue trend (last 6 months)
|
||||
@monthly_revenue = (0..5).map do |months_ago|
|
||||
start_date = months_ago.months.ago.beginning_of_month
|
||||
end_date = months_ago.months.ago.end_of_month
|
||||
revenue = current_user.events
|
||||
.joins(:orders)
|
||||
.where(orders: { status: [ "paid", "completed" ] })
|
||||
.where(orders: { created_at: start_date..end_date })
|
||||
.sum("orders.total_amount_cents") / 100.0
|
||||
|
||||
{
|
||||
month: start_date.strftime("%B %Y"),
|
||||
revenue: revenue
|
||||
}
|
||||
end.reverse
|
||||
end
|
||||
|
||||
# Simplified upcoming events preview - only show if user has orders
|
||||
if @user_orders.any?
|
||||
ordered_event_ids = @user_orders.map(&:event).map(&:id)
|
||||
@upcoming_preview_events = Event.published
|
||||
.upcoming
|
||||
.where.not(id: ordered_event_ids)
|
||||
.order(start_time: :asc)
|
||||
.limit(6)
|
||||
else
|
||||
@upcoming_preview_events = []
|
||||
end
|
||||
end
|
||||
|
||||
# Events page showing all published events with pagination
|
||||
|
||||
@@ -111,7 +111,7 @@ class Promoter::EventsController < ApplicationController
|
||||
params.require(:event).permit(
|
||||
:name, :slug, :description, :image,
|
||||
:venue_name, :venue_address, :latitude, :longitude,
|
||||
:start_time, :end_time, :featured
|
||||
:start_time, :end_time, :featured, :allow_booking_during_event
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
26
app/controllers/settings_controller.rb
Normal file
26
app/controllers/settings_controller.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
class SettingsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_user
|
||||
|
||||
def show
|
||||
# Show settings page
|
||||
end
|
||||
|
||||
def update
|
||||
if @user.update(user_params)
|
||||
redirect_to settings_path, notice: "Vos informations ont été mises à jour avec succès."
|
||||
else
|
||||
render :show, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = current_user
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:first_name, :last_name, :is_professionnal)
|
||||
end
|
||||
end
|
||||
@@ -3,9 +3,10 @@
|
||||
# This controller now primarily handles legacy redirects and backward compatibility
|
||||
# Most ticket creation functionality has been moved to OrdersController
|
||||
class TicketsController < ApplicationController
|
||||
before_action :authenticate_user!, only: [ :payment_success, :payment_cancel, :show, :ticket_view, :download_ticket ]
|
||||
before_action :authenticate_user!, only: [ :payment_success, :payment_cancel, :show, :download ]
|
||||
before_action :set_event, only: [ :checkout, :retry_payment ]
|
||||
|
||||
|
||||
# Redirect to order-based checkout
|
||||
def checkout
|
||||
# Check for draft order
|
||||
@@ -49,28 +50,18 @@ class TicketsController < ApplicationController
|
||||
|
||||
# Display ticket details
|
||||
def show
|
||||
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user).find_by(
|
||||
tickets: { id: params[:ticket_id] },
|
||||
orders: { user_id: current_user.id }
|
||||
)
|
||||
@event = @ticket.event
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé"
|
||||
end
|
||||
|
||||
# Display ticket in PDF-like format
|
||||
def ticket_view
|
||||
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user).find_by(
|
||||
tickets: { id: params[:ticket_id] },
|
||||
orders: { user_id: current_user.id }
|
||||
)
|
||||
# Find ticket by qr code id
|
||||
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user)
|
||||
.find_by(tickets: { qr_code: params[:qr_code] })
|
||||
|
||||
if @ticket.nil?
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé ou vous n'avez pas l'autorisation d'accéder à ce billet"
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé"
|
||||
return
|
||||
end
|
||||
|
||||
@event = @ticket.event
|
||||
@order = @ticket.order
|
||||
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé"
|
||||
end
|
||||
@@ -78,104 +69,30 @@ class TicketsController < ApplicationController
|
||||
# Download PDF ticket - only accessible by ticket owner
|
||||
# User must be authenticated to download ticket
|
||||
# TODO: change ID to an unique identifier (UUID)
|
||||
def download_ticket
|
||||
# Find ticket and ensure it belongs to current user
|
||||
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user).find_by(
|
||||
tickets: { id: params[:ticket_id] },
|
||||
orders: { user_id: current_user.id }
|
||||
)
|
||||
def download
|
||||
# Find ticket by qr code id
|
||||
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user)
|
||||
.find_by(tickets: { qr_code: params[:qr_code] })
|
||||
|
||||
if @ticket.nil?
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé ou vous n'avez pas l'autorisation d'accéder à ce billet"
|
||||
return
|
||||
end
|
||||
|
||||
# Generate PDF using Grover
|
||||
begin
|
||||
Rails.logger.info "Starting PDF generation for ticket ID: #{@ticket.id}"
|
||||
|
||||
# Render the HTML template
|
||||
html = render_to_string(
|
||||
partial: "tickets/pdf_ticket",
|
||||
layout: false,
|
||||
locals: { ticket: @ticket }
|
||||
)
|
||||
|
||||
Rails.logger.info "HTML template rendered successfully, length: #{html.length}"
|
||||
|
||||
# Try to load and use Grover
|
||||
begin
|
||||
Rails.logger.info "Attempting to load Grover gem"
|
||||
|
||||
# Try different approaches to load grover
|
||||
begin
|
||||
require "bundler"
|
||||
Bundler.require(:default, Rails.env)
|
||||
Rails.logger.info "Bundler required gems successfully"
|
||||
rescue => bundler_error
|
||||
Rails.logger.warn "Bundler require failed: #{bundler_error.message}"
|
||||
end
|
||||
|
||||
# Direct path approach using bundle show
|
||||
grover_gem_path = `bundle show grover`.strip
|
||||
grover_path = File.join(grover_gem_path, "lib", "grover")
|
||||
|
||||
if File.exist?(grover_path + ".rb")
|
||||
Rails.logger.info "Loading Grover from direct path: #{grover_path}"
|
||||
require grover_path
|
||||
else
|
||||
Rails.logger.error "Grover not found at path: #{grover_path}"
|
||||
raise LoadError, "Grover gem not available at expected path"
|
||||
end
|
||||
|
||||
Rails.logger.info "Creating Grover instance with options"
|
||||
grover = Grover.new(html,
|
||||
format: "A6",
|
||||
margin: {
|
||||
top: "10mm",
|
||||
bottom: "10mm",
|
||||
left: "10mm",
|
||||
right: "10mm"
|
||||
},
|
||||
prefer_css_page_size: true,
|
||||
emulate_media: "print",
|
||||
cache: false,
|
||||
launch_args: [ "--no-sandbox", "--disable-setuid-sandbox" ] # For better compatibility
|
||||
)
|
||||
Rails.logger.info "Grover instance created successfully"
|
||||
|
||||
pdf_content = grover.to_pdf
|
||||
Rails.logger.info "PDF generated successfully, length: #{pdf_content.length}"
|
||||
# Generate PDF
|
||||
pdf_content = @ticket.to_pdf
|
||||
|
||||
# Send PDF as download
|
||||
send_data pdf_content,
|
||||
filename: "ticket_#{@ticket.id}_#{@ticket.event.name.parameterize}.pdf",
|
||||
type: "application/pdf",
|
||||
disposition: "attachment"
|
||||
rescue LoadError => grover_error
|
||||
Rails.logger.error "Failed to load Grover: #{grover_error.message}"
|
||||
# Fallback: return HTML instead of PDF
|
||||
send_data html,
|
||||
filename: "ticket_#{@ticket.id}_#{@ticket.event.name.parameterize}.html",
|
||||
type: "text/html",
|
||||
disposition: "attachment"
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error "Error generating ticket PDF with Grover:"
|
||||
Rails.logger.error "Message: #{e.message}"
|
||||
Rails.logger.error "Backtrace: #{e.backtrace.join("\n")}"
|
||||
redirect_to dashboard_path, alert: "Erreur lors de la génération du billet"
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
Rails.logger.error "ActiveRecord::RecordNotFound error: #{e.message}"
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé"
|
||||
rescue => e
|
||||
Rails.logger.error "Unexpected error in download_ticket action:"
|
||||
Rails.logger.error "Message: #{e.message}"
|
||||
Rails.logger.error "Backtrace: #{e.backtrace.join("\n")}"
|
||||
Rails.logger.error "Error generating ticket PDF: #{e.message}"
|
||||
redirect_to dashboard_path, alert: "Erreur lors de la génération du billet"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_event
|
||||
|
||||
2
app/helpers/onboarding_helper.rb
Normal file
2
app/helpers/onboarding_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module OnboardingHelper
|
||||
end
|
||||
667
app/javascript/controllers/event_form_controller.js
Normal file
667
app/javascript/controllers/event_form_controller.js
Normal file
@@ -0,0 +1,667 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import slug from 'slug'
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["name", "slug", "latitude", "longitude", "address", "mapLinksContainer", "geocodingSpinner", "getCurrentLocationBtn", "getCurrentLocationIcon", "getCurrentLocationText", "previewLocationBtn", "previewLocationIcon", "previewLocationText", "messagesContainer"]
|
||||
static values = {
|
||||
geocodeDelay: { type: Number, default: 1500 } // Delay before auto-geocoding
|
||||
}
|
||||
|
||||
static lastGeocodingRequest = 0
|
||||
|
||||
connect() {
|
||||
this.geocodeTimeout = null
|
||||
this.isManualGeocodingInProgress = false
|
||||
|
||||
// Initialize map links if we have an address and coordinates already exist
|
||||
if (this.hasAddressTarget && this.addressTarget.value.trim() &&
|
||||
this.hasLatitudeTarget && this.hasLongitudeTarget &&
|
||||
this.latitudeTarget.value && this.longitudeTarget.value) {
|
||||
this.updateMapLinks()
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.geocodeTimeout) {
|
||||
clearTimeout(this.geocodeTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate slug from name
|
||||
generateSlug() {
|
||||
const name = this.nameTarget.value
|
||||
|
||||
this.slugTarget.value = slug(name)
|
||||
}
|
||||
|
||||
// Handle address changes with debounced geocoding
|
||||
addressChanged() {
|
||||
// Clear any existing timeout
|
||||
if (this.geocodeTimeout) {
|
||||
clearTimeout(this.geocodeTimeout)
|
||||
}
|
||||
|
||||
const address = this.addressTarget.value.trim()
|
||||
|
||||
if (!address) {
|
||||
this.clearCoordinates()
|
||||
this.clearMapLinks()
|
||||
this.hideGeocodingSpinner()
|
||||
return
|
||||
}
|
||||
|
||||
// Show spinner after a brief delay to avoid flickering for very short typing
|
||||
const showSpinnerTimeout = setTimeout(() => {
|
||||
this.showGeocodingSpinner()
|
||||
}, 300)
|
||||
|
||||
// Debounce geocoding to avoid too many API calls
|
||||
this.geocodeTimeout = setTimeout(async () => {
|
||||
clearTimeout(showSpinnerTimeout) // Cancel spinner delay if still pending
|
||||
this.showGeocodingSpinner() // Show spinner for sure now
|
||||
|
||||
try {
|
||||
await this.geocodeAddressQuiet(address)
|
||||
} finally {
|
||||
this.hideGeocodingSpinner()
|
||||
}
|
||||
}, this.geocodeDelayValue)
|
||||
}
|
||||
|
||||
// Get user's current location and reverse geocode to address
|
||||
async getCurrentLocation() {
|
||||
if (!navigator.geolocation) {
|
||||
this.showLocationError("La géolocalisation n'est pas supportée par ce navigateur.")
|
||||
return
|
||||
}
|
||||
|
||||
this.showGetCurrentLocationLoading()
|
||||
this.showLocationLoading()
|
||||
|
||||
const options = {
|
||||
enableHighAccuracy: true,
|
||||
timeout: 10000,
|
||||
maximumAge: 60000
|
||||
}
|
||||
|
||||
try {
|
||||
const position = await this.getCurrentPositionPromise(options)
|
||||
const lat = position.coords.latitude
|
||||
const lng = position.coords.longitude
|
||||
|
||||
// Set coordinates first
|
||||
this.latitudeTarget.value = lat.toFixed(6)
|
||||
this.longitudeTarget.value = lng.toFixed(6)
|
||||
|
||||
// Then reverse geocode to get address
|
||||
const address = await this.reverseGeocode(lat, lng)
|
||||
|
||||
if (address) {
|
||||
this.addressTarget.value = address
|
||||
this.showLocationSuccess("Position actuelle détectée et adresse mise à jour!")
|
||||
} else {
|
||||
this.showLocationSuccess("Position actuelle détectée!")
|
||||
}
|
||||
|
||||
this.updateMapLinks()
|
||||
this.hideGetCurrentLocationLoading()
|
||||
|
||||
} catch (error) {
|
||||
this.hideGetCurrentLocationLoading()
|
||||
this.hideLocationLoading()
|
||||
let message = "Erreur lors de la récupération de la localisation."
|
||||
|
||||
switch(error.code) {
|
||||
case error.PERMISSION_DENIED:
|
||||
message = "L'accès à la localisation a été refusé."
|
||||
break
|
||||
case error.POSITION_UNAVAILABLE:
|
||||
message = "Les informations de localisation ne sont pas disponibles."
|
||||
break
|
||||
case error.TIMEOUT:
|
||||
message = "La demande de localisation a expiré."
|
||||
break
|
||||
}
|
||||
|
||||
this.showLocationError(message)
|
||||
} finally {
|
||||
this.hideGetCurrentLocationLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// Promise wrapper for geolocation
|
||||
getCurrentPositionPromise(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
navigator.geolocation.getCurrentPosition(resolve, reject, options)
|
||||
})
|
||||
}
|
||||
|
||||
// Reverse geocode coordinates to get address
|
||||
async reverseGeocode(lat, lng) {
|
||||
try {
|
||||
const response = await fetch(`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&addressdetails=1`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'AperoNight Event Platform/1.0 (https://aperonight.com)',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log('Reverse geocoding response:', data) // Debug log
|
||||
|
||||
if (data && data.display_name) {
|
||||
return data.display_name
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
console.log("Reverse geocoding failed:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Preview location - same as updating map links but with user feedback
|
||||
previewLocation() {
|
||||
if (!this.hasAddressTarget || !this.addressTarget.value.trim()) {
|
||||
this.showLocationError("Veuillez saisir une adresse pour la prévisualiser.")
|
||||
return
|
||||
}
|
||||
|
||||
// If we already have coordinates, just update map links
|
||||
if (this.hasLatitudeTarget && this.hasLongitudeTarget &&
|
||||
this.latitudeTarget.value && this.longitudeTarget.value) {
|
||||
this.updateMapLinks()
|
||||
this.showLocationSuccess("Liens de carte mis à jour!")
|
||||
} else {
|
||||
// Otherwise geocode the address first
|
||||
this.showPreviewLocationLoading()
|
||||
this.geocodeAddress().finally(() => {
|
||||
this.hidePreviewLocationLoading()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Geocode address manually (with user feedback)
|
||||
async geocodeAddress() {
|
||||
if (!this.hasAddressTarget || !this.addressTarget.value.trim()) {
|
||||
this.showLocationError("Veuillez saisir une adresse.")
|
||||
return
|
||||
}
|
||||
|
||||
const address = this.addressTarget.value.trim()
|
||||
|
||||
try {
|
||||
this.isManualGeocodingInProgress = true
|
||||
this.showLocationLoading()
|
||||
const result = await this.performGeocode(address)
|
||||
|
||||
if (result) {
|
||||
this.latitudeTarget.value = result.lat
|
||||
this.longitudeTarget.value = result.lng
|
||||
this.updateMapLinks()
|
||||
|
||||
if (result.accuracy === 'exact') {
|
||||
this.showLocationSuccess("Coordonnées exactes trouvées pour cette adresse!")
|
||||
} else {
|
||||
this.showLocationSuccess(`Coordonnées approximatives trouvées: ${result.display_name}`)
|
||||
}
|
||||
} else {
|
||||
this.showLocationError("Impossible de trouver les coordonnées pour cette adresse.")
|
||||
}
|
||||
} catch (error) {
|
||||
this.showLocationError("Erreur lors de la recherche de l'adresse.")
|
||||
} finally {
|
||||
this.isManualGeocodingInProgress = false
|
||||
this.hideLocationLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// Geocode address quietly (no user feedback, for auto-geocoding)
|
||||
async geocodeAddressQuiet(address) {
|
||||
// Skip if address is too short or invalid
|
||||
if (!address || address.length < 5) {
|
||||
this.clearCoordinates()
|
||||
this.clearMapLinks()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.performGeocode(address)
|
||||
|
||||
if (result && result.lat && result.lng) {
|
||||
this.latitudeTarget.value = result.lat
|
||||
this.longitudeTarget.value = result.lng
|
||||
this.updateMapLinks()
|
||||
console.log(`Auto-geocoded "${address}" to ${result.lat}, ${result.lng}`)
|
||||
|
||||
// Show success message based on accuracy
|
||||
if (result.accuracy === 'exact') {
|
||||
this.showGeocodingSuccess("Adresse géolocalisée avec précision", result.display_name)
|
||||
} else {
|
||||
this.showGeocodingSuccess("Adresse géolocalisée approximativement", result.display_name)
|
||||
}
|
||||
} else {
|
||||
// If auto-geocoding fails, show a subtle warning
|
||||
this.showGeocodingWarning(address)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Auto-geocoding failed:", error)
|
||||
this.showGeocodingWarning(address)
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the actual geocoding request with fallback strategies
|
||||
async performGeocode(address) {
|
||||
// Rate limiting: ensure at least 1 second between requests
|
||||
const now = Date.now()
|
||||
const timeSinceLastRequest = now - (this.constructor.lastGeocodingRequest || 0)
|
||||
if (timeSinceLastRequest < 1000) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 - timeSinceLastRequest))
|
||||
}
|
||||
this.constructor.lastGeocodingRequest = Date.now()
|
||||
|
||||
// Try multiple geocoding strategies
|
||||
const strategies = [
|
||||
// Strategy 1: Exact address
|
||||
address,
|
||||
// Strategy 2: Street name + city (remove house number)
|
||||
address.replace(/^\d+\s*/, ''),
|
||||
// Strategy 3: Just city and postal code
|
||||
this.extractCityAndPostalCode(address)
|
||||
].filter(Boolean) // Remove null/undefined values
|
||||
|
||||
for (let i = 0; i < strategies.length; i++) {
|
||||
const searchAddress = strategies[i]
|
||||
console.log(`Geocoding attempt ${i + 1}: "${searchAddress}"`)
|
||||
|
||||
// Show progress for manual geocoding (not auto-geocoding)
|
||||
if (this.isManualGeocodingInProgress) {
|
||||
const strategyNames = ['adresse complète', 'rue et ville', 'ville seulement']
|
||||
this.showGeocodingProgress(strategyNames[i] || `stratégie ${i + 1}`, `${i + 1}/${strategies.length}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.tryGeocode(searchAddress)
|
||||
if (result) {
|
||||
console.log(`Geocoding successful with strategy ${i + 1}`)
|
||||
this.hideMessage("geocoding-progress")
|
||||
return result
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Strategy ${i + 1} failed:`, error.message)
|
||||
}
|
||||
|
||||
// Add small delay between attempts
|
||||
if (i < strategies.length - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
}
|
||||
}
|
||||
|
||||
this.hideMessage("geocoding-progress")
|
||||
|
||||
console.log('All geocoding strategies failed')
|
||||
return null
|
||||
}
|
||||
|
||||
// Extract city and postal code from address
|
||||
extractCityAndPostalCode(address) {
|
||||
// Look for French postal code pattern (5 digits) + city
|
||||
const match = address.match(/(\d{5})\s+([^,]+)/);
|
||||
if (match) {
|
||||
return `${match[1]} ${match[2].trim()}`
|
||||
}
|
||||
|
||||
// Fallback: extract last part after comma (assume it's city)
|
||||
const parts = address.split(',')
|
||||
if (parts.length > 1) {
|
||||
return parts[parts.length - 1].trim()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Try a single geocoding request
|
||||
async tryGeocode(address) {
|
||||
const encodedAddress = encodeURIComponent(address.trim())
|
||||
|
||||
const response = await fetch(`https://nominatim.openstreetmap.org/search?q=${encodedAddress}&format=json&limit=1&addressdetails=1`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'AperoNight Event Platform/1.0 (https://aperonight.com)',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data && data.length > 0) {
|
||||
const result = data[0]
|
||||
return {
|
||||
lat: parseFloat(result.lat).toFixed(6),
|
||||
lng: parseFloat(result.lon).toFixed(6),
|
||||
display_name: result.display_name,
|
||||
accuracy: address === result.display_name ? 'exact' : 'approximate'
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Update map links based on current coordinates
|
||||
updateMapLinks() {
|
||||
if (!this.hasMapLinksContainerTarget) return
|
||||
|
||||
const lat = parseFloat(this.latitudeTarget.value)
|
||||
const lng = parseFloat(this.longitudeTarget.value)
|
||||
const address = this.hasAddressTarget ? this.addressTarget.value.trim() : ""
|
||||
|
||||
if (isNaN(lat) || isNaN(lng) || !address) {
|
||||
this.clearMapLinks()
|
||||
return
|
||||
}
|
||||
|
||||
const links = this.generateMapLinks(lat, lng, address)
|
||||
this.mapLinksContainerTarget.innerHTML = links
|
||||
}
|
||||
|
||||
// Generate map links HTML
|
||||
generateMapLinks(lat, lng, address) {
|
||||
const encodedAddress = encodeURIComponent(address)
|
||||
|
||||
const providers = {
|
||||
google: {
|
||||
name: "Google Maps",
|
||||
url: `https://www.google.com/maps/search/${encodedAddress},16z`,
|
||||
icon: "🔍"
|
||||
},
|
||||
openstreetmap: {
|
||||
name: "OpenStreetMap",
|
||||
url: `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lng}#map=16/${lat}/${lng}`,
|
||||
icon: "🗺️"
|
||||
},
|
||||
apple: {
|
||||
name: "Apple Plans",
|
||||
url: `https://maps.apple.com/?address=${encodedAddress}&ll=${lat},${lng}`,
|
||||
icon: "🍎"
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<i data-lucide="map-pin" class="w-4 h-4 text-gray-500"></i>
|
||||
<span class="text-sm font-medium text-gray-700">Voir sur la carte :</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
${Object.entries(providers).map(([key, provider]) => `
|
||||
<a href="${provider.url}" target="_blank" rel="noopener"
|
||||
class="inline-flex items-center px-3 py-2 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<span class="mr-2">${provider.icon}</span>
|
||||
${provider.name}
|
||||
</a>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
// Clear coordinates
|
||||
clearCoordinates() {
|
||||
if (this.hasLatitudeTarget) this.latitudeTarget.value = ""
|
||||
if (this.hasLongitudeTarget) this.longitudeTarget.value = ""
|
||||
}
|
||||
|
||||
// Clear map links
|
||||
clearMapLinks() {
|
||||
if (this.hasMapLinksContainerTarget) {
|
||||
this.mapLinksContainerTarget.innerHTML = ""
|
||||
}
|
||||
}
|
||||
|
||||
// Show geocoding spinner in address input
|
||||
showGeocodingSpinner() {
|
||||
if (this.hasGeocodingSpinnerTarget) {
|
||||
this.geocodingSpinnerTarget.classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
|
||||
// Hide geocoding spinner in address input
|
||||
hideGeocodingSpinner() {
|
||||
if (this.hasGeocodingSpinnerTarget) {
|
||||
this.geocodingSpinnerTarget.classList.add('hidden')
|
||||
}
|
||||
}
|
||||
|
||||
// Show loading state on "Ma position" button
|
||||
showGetCurrentLocationLoading() {
|
||||
if (this.hasGetCurrentLocationBtnTarget) {
|
||||
this.getCurrentLocationBtnTarget.disabled = true
|
||||
}
|
||||
if (this.hasGetCurrentLocationIconTarget) {
|
||||
this.getCurrentLocationIconTarget.innerHTML = '<div class="w-3 h-3 mr-1 border border-white border-t-transparent rounded-full animate-spin"></div>'
|
||||
}
|
||||
if (this.hasGetCurrentLocationTextTarget) {
|
||||
this.getCurrentLocationTextTarget.textContent = 'Localisation...'
|
||||
}
|
||||
}
|
||||
|
||||
// Hide loading state on "Ma position" button
|
||||
hideGetCurrentLocationLoading() {
|
||||
if (this.hasGetCurrentLocationBtnTarget) {
|
||||
this.getCurrentLocationBtnTarget.disabled = false
|
||||
}
|
||||
if (this.hasGetCurrentLocationIconTarget) {
|
||||
this.getCurrentLocationIconTarget.innerHTML = '<i data-lucide="map-pin" class="w-3 h-3 mr-1"></i>'
|
||||
// Re-initialize Lucide icons
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons()
|
||||
}
|
||||
}
|
||||
if (this.hasGetCurrentLocationTextTarget) {
|
||||
this.getCurrentLocationTextTarget.textContent = 'Ma position'
|
||||
}
|
||||
}
|
||||
|
||||
// Show loading state on "Prévisualiser" button
|
||||
showPreviewLocationLoading() {
|
||||
if (this.hasPreviewLocationBtnTarget) {
|
||||
this.previewLocationBtnTarget.disabled = true
|
||||
}
|
||||
if (this.hasPreviewLocationIconTarget) {
|
||||
this.previewLocationIconTarget.innerHTML = '<div class="w-3 h-3 mr-1 border border-purple-700 border-t-transparent rounded-full animate-spin"></div>'
|
||||
}
|
||||
if (this.hasPreviewLocationTextTarget) {
|
||||
this.previewLocationTextTarget.textContent = 'Recherche...'
|
||||
}
|
||||
}
|
||||
|
||||
// Hide loading state on "Prévisualiser" button
|
||||
hidePreviewLocationLoading() {
|
||||
if (this.hasPreviewLocationBtnTarget) {
|
||||
this.previewLocationBtnTarget.disabled = false
|
||||
}
|
||||
if (this.hasPreviewLocationIconTarget) {
|
||||
this.previewLocationIconTarget.innerHTML = '<i data-lucide="map" class="w-3 h-3 mr-1"></i>'
|
||||
// Re-initialize Lucide icons
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons()
|
||||
}
|
||||
}
|
||||
if (this.hasPreviewLocationTextTarget) {
|
||||
this.previewLocationTextTarget.textContent = 'Prévisualiser'
|
||||
}
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
showLocationLoading() {
|
||||
this.hideAllLocationMessages()
|
||||
this.showMessage("location-loading", "Géolocalisation en cours...", "loading")
|
||||
}
|
||||
|
||||
// Hide loading state
|
||||
hideLocationLoading() {
|
||||
this.hideMessage("location-loading")
|
||||
}
|
||||
|
||||
// Show success message
|
||||
showLocationSuccess(message) {
|
||||
this.hideAllLocationMessages()
|
||||
this.showMessage("location-success", message, "success")
|
||||
setTimeout(() => this.hideMessage("location-success"), 4000)
|
||||
}
|
||||
|
||||
// Show error message
|
||||
showLocationError(message) {
|
||||
this.hideAllLocationMessages()
|
||||
this.showMessage("location-error", message, "error")
|
||||
setTimeout(() => this.hideMessage("location-error"), 6000)
|
||||
}
|
||||
|
||||
// Show geocoding warning (less intrusive than error)
|
||||
showGeocodingWarning(address) {
|
||||
this.hideMessage("geocoding-warning")
|
||||
const message = "Les coordonnées n'ont pas pu être déterminées automatiquement. L'événement utilisera une localisation approximative."
|
||||
this.showMessage("geocoding-warning", message, "warning")
|
||||
setTimeout(() => this.hideMessage("geocoding-warning"), 8000)
|
||||
}
|
||||
|
||||
// Show info about approximate location
|
||||
showApproximateLocationInfo(foundLocation) {
|
||||
this.hideMessage("approximate-location-info")
|
||||
const message = `Localisation approximative trouvée: ${foundLocation}`
|
||||
this.showMessage("approximate-location-info", message, "info")
|
||||
setTimeout(() => this.hideMessage("approximate-location-info"), 6000)
|
||||
}
|
||||
|
||||
// Show geocoding success with location details
|
||||
showGeocodingSuccess(title, location) {
|
||||
this.hideMessage("geocoding-success")
|
||||
const message = `${title}<br><small class="opacity-75">${location}</small>`
|
||||
this.showMessage("geocoding-success", message, "success")
|
||||
setTimeout(() => this.hideMessage("geocoding-success"), 5000)
|
||||
}
|
||||
|
||||
// Show geocoding progress with strategy info
|
||||
showGeocodingProgress(strategy, attempt) {
|
||||
this.hideMessage("geocoding-progress")
|
||||
const message = `Recherche en cours... (${attempt}/${strategy})`
|
||||
this.showMessage("geocoding-progress", message, "loading")
|
||||
}
|
||||
|
||||
// Message template configurations
|
||||
getMessageTemplate(type) {
|
||||
const templates = {
|
||||
info: {
|
||||
bgColor: "bg-blue-50",
|
||||
borderColor: "border-blue-200",
|
||||
textColor: "text-blue-800",
|
||||
icon: "info",
|
||||
iconColor: "text-blue-500"
|
||||
},
|
||||
success: {
|
||||
bgColor: "bg-green-50",
|
||||
borderColor: "border-green-200",
|
||||
textColor: "text-green-800",
|
||||
icon: "check-circle",
|
||||
iconColor: "text-green-500"
|
||||
},
|
||||
error: {
|
||||
bgColor: "bg-red-50",
|
||||
borderColor: "border-red-200",
|
||||
textColor: "text-red-800",
|
||||
icon: "alert-circle",
|
||||
iconColor: "text-red-500"
|
||||
},
|
||||
warning: {
|
||||
bgColor: "bg-yellow-50",
|
||||
borderColor: "border-yellow-200",
|
||||
textColor: "text-yellow-800",
|
||||
icon: "alert-triangle",
|
||||
iconColor: "text-yellow-500"
|
||||
},
|
||||
loading: {
|
||||
bgColor: "bg-purple-50",
|
||||
borderColor: "border-purple-200",
|
||||
textColor: "text-purple-800",
|
||||
icon: "loader-2",
|
||||
iconColor: "text-purple-500",
|
||||
animated: true
|
||||
}
|
||||
}
|
||||
return templates[type] || templates.info
|
||||
}
|
||||
|
||||
// Create dynamic message HTML using template
|
||||
createMessageHTML(id, message, type) {
|
||||
const template = this.getMessageTemplate(type)
|
||||
const animationClass = template.animated ? 'animate-spin' : ''
|
||||
|
||||
return `
|
||||
<div id="${id}" class="flex items-start space-x-3 p-4 ${template.bgColor} ${template.borderColor} border rounded-lg shadow-sm transition-all duration-200 ease-in-out">
|
||||
<div class="flex-shrink-0">
|
||||
<i data-lucide="${template.icon}" class="w-5 h-5 ${template.iconColor} ${animationClass}"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium ${template.textColor} leading-relaxed">${message}</p>
|
||||
</div>
|
||||
<button type="button" onclick="this.parentElement.remove()" class="flex-shrink-0 ${template.textColor} hover:opacity-70 transition-opacity">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
// Show a message with given type using template system
|
||||
showMessage(id, message, type) {
|
||||
// Remove existing message with same ID first
|
||||
this.hideMessage(id)
|
||||
|
||||
const messageHtml = this.createMessageHTML(id, message, type)
|
||||
|
||||
// Insert into the dedicated messages container in the venue section
|
||||
if (this.hasMessagesContainerTarget) {
|
||||
this.messagesContainerTarget.insertAdjacentHTML('beforeend', messageHtml)
|
||||
// Re-initialize Lucide icons for the new elements
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons()
|
||||
}
|
||||
} else {
|
||||
// Fallback: insert before the address input if messages container not found
|
||||
const addressInput = this.hasAddressTarget ? this.addressTarget.parentElement : null
|
||||
if (addressInput) {
|
||||
addressInput.insertAdjacentHTML('beforebegin', messageHtml)
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide a specific message
|
||||
hideMessage(id) {
|
||||
const element = document.getElementById(id)
|
||||
if (element) {
|
||||
element.remove()
|
||||
}
|
||||
}
|
||||
|
||||
// Hide all location messages
|
||||
hideAllLocationMessages() {
|
||||
this.hideMessage("location-loading")
|
||||
this.hideMessage("location-success")
|
||||
this.hideMessage("location-error")
|
||||
this.hideMessage("geocoding-warning")
|
||||
this.hideMessage("approximate-location-info")
|
||||
this.hideMessage("geocoding-success")
|
||||
this.hideMessage("geocoding-progress")
|
||||
}
|
||||
}
|
||||
@@ -18,3 +18,9 @@ application.register("ticket-selection", TicketSelectionController);
|
||||
|
||||
import HeaderController from "./header_controller";
|
||||
application.register("header", HeaderController);
|
||||
|
||||
import QrCodeController from "./qr_code_controller";
|
||||
application.register("qr-code", QrCodeController);
|
||||
|
||||
import EventFormController from "./event_form_controller";
|
||||
application.register("event-form", EventFormController);
|
||||
|
||||
56
app/javascript/controllers/qr_code_controller.js
Normal file
56
app/javascript/controllers/qr_code_controller.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// QR Code generator controller using qrcode npm package
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import QRCode from "qrcode"
|
||||
|
||||
export default class extends Controller {
|
||||
static values = { data: String }
|
||||
static targets = ["container", "loading"]
|
||||
|
||||
connect() {
|
||||
this.generateQRCode()
|
||||
}
|
||||
|
||||
async generateQRCode() {
|
||||
try {
|
||||
// Hide loading indicator
|
||||
if (this.hasLoadingTarget) {
|
||||
this.loadingTarget.style.display = 'none'
|
||||
}
|
||||
|
||||
// Create canvas element
|
||||
const canvas = document.createElement('canvas')
|
||||
|
||||
// Generate QR code using qrcode library
|
||||
await QRCode.toCanvas(canvas, this.dataValue, {
|
||||
width: 128,
|
||||
height: 128,
|
||||
margin: 1,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#FFFFFF'
|
||||
}
|
||||
})
|
||||
|
||||
// Clear container and add QR code
|
||||
this.containerTarget.innerHTML = ''
|
||||
this.containerTarget.appendChild(canvas)
|
||||
|
||||
console.log('QR code generated successfully')
|
||||
} catch (error) {
|
||||
console.error('Error generating QR code:', error)
|
||||
this.showFallback()
|
||||
}
|
||||
}
|
||||
|
||||
showFallback() {
|
||||
this.containerTarget.innerHTML = `
|
||||
<div class="w-32 h-32 bg-gray-100 rounded flex items-center justify-center text-gray-500 text-xs border-2 border-dashed border-gray-300">
|
||||
<div class="text-center">
|
||||
<div class="text-lg mb-1">📱</div>
|
||||
<div>QR Code</div>
|
||||
<div class="font-mono text-xs mt-1 break-all px-2">${this.dataValue}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
19
app/jobs/event_reminder_job.rb
Normal file
19
app/jobs/event_reminder_job.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
class EventReminderJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(event_id, days_before)
|
||||
event = Event.find(event_id)
|
||||
|
||||
# Find all users with active tickets for this event
|
||||
users_with_tickets = User.joins(orders: { tickets: :ticket_type })
|
||||
.where(ticket_types: { event: event })
|
||||
.where(tickets: { status: "active" })
|
||||
.distinct
|
||||
|
||||
users_with_tickets.find_each do |user|
|
||||
TicketMailer.event_reminder(user, event, days_before).deliver_now
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Failed to send event reminder to user #{user.id} for event #{event.id}: #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
44
app/jobs/event_reminder_scheduler_job.rb
Normal file
44
app/jobs/event_reminder_scheduler_job.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
class EventReminderSchedulerJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform
|
||||
schedule_weekly_reminders
|
||||
schedule_daily_reminders
|
||||
schedule_day_of_reminders
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def schedule_weekly_reminders
|
||||
# Find events starting in exactly 7 days
|
||||
target_date = 7.days.from_now.beginning_of_day
|
||||
events = Event.published
|
||||
.where(start_time: target_date..(target_date + 1.day))
|
||||
|
||||
events.find_each do |event|
|
||||
EventReminderJob.perform_later(event.id, 7)
|
||||
end
|
||||
end
|
||||
|
||||
def schedule_daily_reminders
|
||||
# Find events starting in exactly 1 day (tomorrow)
|
||||
target_date = 1.day.from_now.beginning_of_day
|
||||
events = Event.published
|
||||
.where(start_time: target_date..(target_date + 1.day))
|
||||
|
||||
events.find_each do |event|
|
||||
EventReminderJob.perform_later(event.id, 1)
|
||||
end
|
||||
end
|
||||
|
||||
def schedule_day_of_reminders
|
||||
# Find events starting today
|
||||
target_date = Time.current.beginning_of_day
|
||||
events = Event.published
|
||||
.where(start_time: target_date..(target_date + 1.day))
|
||||
|
||||
events.find_each do |event|
|
||||
EventReminderJob.perform_later(event.id, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
class ApplicationMailer < ActionMailer::Base
|
||||
default from: "from@example.com"
|
||||
default from: ENV.fetch("MAILER_FROM_EMAIL", "no-reply@aperonight.fr")
|
||||
layout "mailer"
|
||||
end
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
class TicketMailer < ApplicationMailer
|
||||
default from: "notifications@aperonight.com"
|
||||
def purchase_confirmation_order(order)
|
||||
@order = order
|
||||
@user = order.user
|
||||
@event = order.event
|
||||
@tickets = order.tickets
|
||||
|
||||
# Generate PDF attachments for all tickets
|
||||
@tickets.each do |ticket|
|
||||
begin
|
||||
pdf = ticket.to_pdf
|
||||
attachments["ticket-#{@event.name.parameterize}-#{ticket.qr_code[0..7]}.pdf"] = {
|
||||
mime_type: "application/pdf",
|
||||
content: pdf
|
||||
}
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Failed to generate PDF for ticket #{ticket.id}: #{e.message}"
|
||||
# Continue without PDF attachment rather than failing the entire email
|
||||
end
|
||||
end
|
||||
|
||||
mail(
|
||||
to: @user.email,
|
||||
subject: "Confirmation d'achat - #{@event.name}",
|
||||
template_name: "purchase_confirmation"
|
||||
)
|
||||
end
|
||||
|
||||
def purchase_confirmation(ticket)
|
||||
@ticket = ticket
|
||||
@@ -7,15 +32,49 @@ class TicketMailer < ApplicationMailer
|
||||
@event = ticket.event
|
||||
|
||||
# Generate PDF attachment
|
||||
begin
|
||||
pdf = @ticket.to_pdf
|
||||
attachments["ticket-#{@event.name.parameterize}-#{@ticket.qr_code[0..7]}.pdf"] = {
|
||||
mime_type: "application/pdf",
|
||||
content: pdf
|
||||
}
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Failed to generate PDF for ticket #{@ticket.id}: #{e.message}"
|
||||
# Continue without PDF attachment rather than failing the entire email
|
||||
end
|
||||
|
||||
mail(
|
||||
to: @user.email,
|
||||
subject: "Confirmation d'achat - #{@event.name}"
|
||||
)
|
||||
end
|
||||
|
||||
def event_reminder(user, event, days_before)
|
||||
@user = user
|
||||
@event = event
|
||||
@days_before = days_before
|
||||
|
||||
# Get user's tickets for this event
|
||||
@tickets = Ticket.joins(:order, :ticket_type)
|
||||
.where(orders: { user: @user }, ticket_types: { event: @event }, status: "active")
|
||||
|
||||
return if @tickets.empty?
|
||||
|
||||
subject = case days_before
|
||||
when 7
|
||||
"Rappel : #{@event.name} dans une semaine"
|
||||
when 1
|
||||
"Rappel : #{@event.name} demain"
|
||||
when 0
|
||||
"C'est aujourd'hui : #{@event.name}"
|
||||
else
|
||||
"Rappel : #{@event.name} dans #{days_before} jours"
|
||||
end
|
||||
|
||||
mail(
|
||||
to: @user.email,
|
||||
subject: subject,
|
||||
template_name: "event_reminder"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Event model representing nightlife events and events
|
||||
# Manages event details, location data, and publication state
|
||||
require "net/http"
|
||||
require "json"
|
||||
|
||||
class Event < ApplicationRecord
|
||||
# Define states for Event lifecycle management
|
||||
# draft: Initial state when Event is being created
|
||||
@@ -19,11 +22,14 @@ class Event < ApplicationRecord
|
||||
has_many :tickets, through: :ticket_types
|
||||
has_many :orders
|
||||
|
||||
# === Callbacks ===
|
||||
before_validation :geocode_address, if: :should_geocode_address?
|
||||
|
||||
# Validations for Event attributes
|
||||
# Basic information
|
||||
validates :name, presence: true, length: { minimum: 3, maximum: 100 }
|
||||
validates :slug, presence: true, length: { minimum: 3, maximum: 100 }
|
||||
validates :description, presence: true, length: { minimum: 10, maximum: 1000 }
|
||||
validates :description, presence: true, length: { minimum: 10, maximum: 2000 }
|
||||
validates :state, presence: true, inclusion: { in: states.keys }
|
||||
validates :image, length: { maximum: 500 } # URL or path to image
|
||||
|
||||
@@ -48,4 +54,184 @@ class Event < ApplicationRecord
|
||||
|
||||
# Scope for published events ordered by start time
|
||||
scope :upcoming, -> { published.where("start_time >= ?", Time.current).order(start_time: :asc) }
|
||||
|
||||
|
||||
# === Instance Methods ===
|
||||
|
||||
# Check if coordinates were successfully geocoded or are fallback coordinates
|
||||
def geocoding_successful?
|
||||
coordinates_look_valid?
|
||||
end
|
||||
|
||||
# Get a user-friendly status message about geocoding
|
||||
def geocoding_status_message
|
||||
return nil if geocoding_successful?
|
||||
|
||||
"Les coordonnées exactes n'ont pas pu être déterminées automatiquement. Une localisation approximative a été utilisée."
|
||||
end
|
||||
|
||||
# Check if ticket booking is currently allowed for this event
|
||||
def booking_allowed?
|
||||
return false unless published?
|
||||
return false if sold_out?
|
||||
return false if canceled?
|
||||
|
||||
# Check if event has started and if booking during event is disabled
|
||||
if event_started? && !allow_booking_during_event?
|
||||
return false
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
# Check if the event has already started
|
||||
def event_started?
|
||||
return false if start_time.blank?
|
||||
Time.current >= start_time
|
||||
end
|
||||
|
||||
# Check if the event has ended
|
||||
def event_ended?
|
||||
return false if end_time.blank?
|
||||
Time.current >= end_time
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Determine if we should perform server-side geocoding
|
||||
def should_geocode_address?
|
||||
# Don't geocode if address is blank
|
||||
return false if venue_address.blank?
|
||||
|
||||
# Don't geocode if we already have valid coordinates (likely from frontend)
|
||||
return false if coordinates_look_valid?
|
||||
|
||||
# Only geocode if address changed and we don't have coordinates
|
||||
venue_address_changed?
|
||||
end
|
||||
|
||||
# Check if the current coordinates look like they were set by frontend geocoding
|
||||
def coordinates_look_valid?
|
||||
return false if latitude.blank? || longitude.blank?
|
||||
|
||||
lat_f = latitude.to_f
|
||||
lng_f = longitude.to_f
|
||||
|
||||
# Basic sanity checks for coordinate ranges
|
||||
return false if lat_f < -90 || lat_f > 90
|
||||
return false if lng_f < -180 || lng_f > 180
|
||||
|
||||
# Check if coordinates are not the default fallback coordinates
|
||||
fallback_lat = 46.603354
|
||||
fallback_lng = 1.888334
|
||||
|
||||
# Check if coordinates are not exactly 0,0 (common invalid default)
|
||||
return false if lat_f == 0.0 && lng_f == 0.0
|
||||
|
||||
# Coordinates are valid if they're not exactly the fallback coordinates
|
||||
!(lat_f == fallback_lat && lng_f == fallback_lng)
|
||||
end
|
||||
|
||||
# Automatically geocode address to get latitude and longitude
|
||||
# This only runs when no valid coordinates are provided (fallback for non-JS users)
|
||||
def geocode_address
|
||||
Rails.logger.info "Running server-side geocoding for '#{venue_address}' (no frontend coordinates provided)"
|
||||
|
||||
# Store original coordinates in case we need to fall back
|
||||
original_lat = latitude
|
||||
original_lng = longitude
|
||||
|
||||
begin
|
||||
# Use OpenStreetMap Nominatim API for geocoding
|
||||
encoded_address = URI.encode_www_form_component(venue_address.strip)
|
||||
uri = URI("https://nominatim.openstreetmap.org/search?q=#{encoded_address}&format=json&limit=1&addressdetails=1")
|
||||
|
||||
http = Net::HTTP.new(uri.host, uri.port)
|
||||
http.use_ssl = true
|
||||
|
||||
request = Net::HTTP::Get.new(uri)
|
||||
request["User-Agent"] = "AperoNight Event Platform/1.0 (https://aperonight.com)"
|
||||
request["Accept"] = "application/json"
|
||||
|
||||
response = http.request(request)
|
||||
|
||||
if response.code == "200"
|
||||
data = JSON.parse(response.body)
|
||||
|
||||
if data.any?
|
||||
result = data.first
|
||||
self.latitude = result["lat"].to_f.round(6)
|
||||
self.longitude = result["lon"].to_f.round(6)
|
||||
Rails.logger.info "Server-side geocoded '#{venue_address}' to coordinates: #{latitude}, #{longitude}"
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
# If we reach here, geocoding failed
|
||||
handle_geocoding_failure(original_lat, original_lng)
|
||||
|
||||
rescue => e
|
||||
Rails.logger.error "Server-side geocoding failed for '#{venue_address}': #{e.message}"
|
||||
handle_geocoding_failure(original_lat, original_lng)
|
||||
end
|
||||
end
|
||||
|
||||
# Handle geocoding failure with fallback strategies
|
||||
def handle_geocoding_failure(original_lat, original_lng)
|
||||
# Strategy 1: Keep existing coordinates if this is an update
|
||||
if original_lat.present? && original_lng.present?
|
||||
self.latitude = original_lat
|
||||
self.longitude = original_lng
|
||||
Rails.logger.warn "Geocoding failed for '#{venue_address}', keeping existing coordinates: #{latitude}, #{longitude}"
|
||||
return
|
||||
end
|
||||
|
||||
# Strategy 2: Try to extract country/city and use approximate coordinates
|
||||
fallback_coordinates = get_fallback_coordinates_from_address
|
||||
if fallback_coordinates
|
||||
self.latitude = fallback_coordinates[:lat]
|
||||
self.longitude = fallback_coordinates[:lng]
|
||||
Rails.logger.warn "Using fallback coordinates for '#{venue_address}': #{latitude}, #{longitude}"
|
||||
return
|
||||
end
|
||||
|
||||
# Strategy 3: Use default coordinates (center of France) as last resort
|
||||
# This ensures the event can still be created
|
||||
self.latitude = 46.603354 # Center of France
|
||||
self.longitude = 1.888334
|
||||
Rails.logger.warn "Using default coordinates for '#{venue_address}' due to geocoding failure: #{latitude}, #{longitude}"
|
||||
end
|
||||
|
||||
# Extract country/city from address and return approximate coordinates
|
||||
def get_fallback_coordinates_from_address
|
||||
address_lower = venue_address.downcase
|
||||
|
||||
# Common French cities with approximate coordinates
|
||||
french_cities = {
|
||||
"paris" => { lat: 48.8566, lng: 2.3522 },
|
||||
"lyon" => { lat: 45.7640, lng: 4.8357 },
|
||||
"marseille" => { lat: 43.2965, lng: 5.3698 },
|
||||
"toulouse" => { lat: 43.6047, lng: 1.4442 },
|
||||
"nice" => { lat: 43.7102, lng: 7.2620 },
|
||||
"nantes" => { lat: 47.2184, lng: -1.5536 },
|
||||
"montpellier" => { lat: 43.6110, lng: 3.8767 },
|
||||
"strasbourg" => { lat: 48.5734, lng: 7.7521 },
|
||||
"bordeaux" => { lat: 44.8378, lng: -0.5792 },
|
||||
"lille" => { lat: 50.6292, lng: 3.0573 }
|
||||
}
|
||||
|
||||
# Check if any known city is mentioned in the address
|
||||
french_cities.each do |city, coords|
|
||||
if address_lower.include?(city)
|
||||
return coords
|
||||
end
|
||||
end
|
||||
|
||||
# Check for common country indicators
|
||||
if address_lower.include?("france") || address_lower.include?("french")
|
||||
return { lat: 46.603354, lng: 1.888334 } # Center of France
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class Order < ApplicationRecord
|
||||
# === Constants ===
|
||||
DRAFT_EXPIRY_TIME = 30.minutes
|
||||
DRAFT_EXPIRY_TIME = 15.minutes
|
||||
MAX_PAYMENT_ATTEMPTS = 3
|
||||
|
||||
# === Associations ===
|
||||
@@ -76,11 +76,23 @@ class Order < ApplicationRecord
|
||||
update!(status: "paid")
|
||||
tickets.update_all(status: "active")
|
||||
end
|
||||
|
||||
# Send purchase confirmation email outside the transaction
|
||||
# so that payment completion isn't affected by email failures
|
||||
begin
|
||||
TicketMailer.purchase_confirmation_order(self).deliver_now
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Failed to send purchase confirmation email for order #{id}: #{e.message}"
|
||||
Rails.logger.error e.backtrace.join("\n")
|
||||
# Don't re-raise the error - payment should still succeed
|
||||
end
|
||||
end
|
||||
|
||||
# Calculate total from tickets
|
||||
# Calculate total from tickets plus 1€ service fee
|
||||
def calculate_total!
|
||||
update!(total_amount_cents: tickets.sum(:price_cents))
|
||||
ticket_total = tickets.sum(:price_cents)
|
||||
fee_cents = 100 # 1€ in cents
|
||||
update!(total_amount_cents: ticket_total + fee_cents)
|
||||
end
|
||||
|
||||
# Create Stripe invoice for accounting records
|
||||
|
||||
@@ -27,29 +27,6 @@ class Ticket < ApplicationRecord
|
||||
TicketPdfGenerator.new(self).generate
|
||||
end
|
||||
|
||||
# Generate QR code data for ticket validation
|
||||
def to_qr_data
|
||||
{
|
||||
ticket_id: id,
|
||||
qr_code: qr_code,
|
||||
event_id: event&.id,
|
||||
user_id: user&.id
|
||||
}.compact.to_json
|
||||
end
|
||||
|
||||
# Generate QR code as SVG
|
||||
def generate_qr_svg
|
||||
require "rqrcode"
|
||||
qrcode = RQRCode::QRCode.new(to_qr_data)
|
||||
qrcode.as_svg(
|
||||
offset: 0,
|
||||
color: "000",
|
||||
shape_rendering: "crispEdges",
|
||||
module_size: 4,
|
||||
standalone: true
|
||||
)
|
||||
end
|
||||
|
||||
# Price in euros (formatted)
|
||||
def price_euros
|
||||
price_cents / 100.0
|
||||
@@ -93,7 +70,6 @@ class Ticket < ApplicationRecord
|
||||
self.qr_code = "#{id || 'temp'}-#{Time.current.to_i}-#{SecureRandom.hex(4)}"
|
||||
end
|
||||
|
||||
|
||||
def draft?
|
||||
status == "draft"
|
||||
end
|
||||
|
||||
@@ -29,11 +29,19 @@ class User < ApplicationRecord
|
||||
validates :first_name, length: { minimum: 2, maximum: 50, allow_blank: true }
|
||||
validates :company_name, length: { minimum: 2, maximum: 100, allow_blank: true }
|
||||
|
||||
# Onboarding methods
|
||||
def needs_onboarding?
|
||||
!onboarding_completed?
|
||||
end
|
||||
|
||||
def complete_onboarding!
|
||||
update!(onboarding_completed: true)
|
||||
end
|
||||
|
||||
# Authorization methods
|
||||
def can_manage_events?
|
||||
# For now, all authenticated users can manage events
|
||||
# This can be extended later with role-based permissions
|
||||
true
|
||||
# Only professional users can manage events
|
||||
is_professionnal?
|
||||
end
|
||||
|
||||
def promoter?
|
||||
|
||||
@@ -103,7 +103,7 @@ class StripeInvoiceService
|
||||
name: customer_name,
|
||||
metadata: {
|
||||
user_id: @order.user.id,
|
||||
created_by: "aperonight_system"
|
||||
created_by: "#{ENV.fetch('INVOICE_COMPANY_NAME', 'aperonight').downcase}_system"
|
||||
}
|
||||
})
|
||||
|
||||
@@ -133,7 +133,7 @@ class StripeInvoiceService
|
||||
order_id: @order.id,
|
||||
user_id: @order.user.id,
|
||||
event_name: @order.event.name,
|
||||
created_by: "aperonight_system",
|
||||
created_by: "#{ENV.fetch('INVOICE_COMPANY_NAME', 'aperonight').downcase}_system",
|
||||
payment_method: "checkout_session"
|
||||
},
|
||||
description: "Invoice for #{@order.event.name} - Order ##{@order.id}",
|
||||
@@ -147,6 +147,7 @@ class StripeInvoiceService
|
||||
end
|
||||
|
||||
def add_line_items_to_invoice(customer, invoice)
|
||||
# Add ticket line items
|
||||
@order.tickets.group_by(&:ticket_type).each do |ticket_type, tickets|
|
||||
quantity = tickets.count
|
||||
|
||||
@@ -164,6 +165,20 @@ class StripeInvoiceService
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
# Add service fee line item
|
||||
service_fee_cents = 100 # 1€ service fee
|
||||
Stripe::InvoiceItem.create({
|
||||
customer: customer.id,
|
||||
invoice: invoice.id,
|
||||
amount: service_fee_cents,
|
||||
currency: "eur",
|
||||
description: "Frais de service - Frais de traitement de la commande",
|
||||
metadata: {
|
||||
item_type: "service_fee",
|
||||
amount_cents: service_fee_cents
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
def build_line_item_description(ticket_type, tickets)
|
||||
|
||||
297
app/services/ticket_pdf_generator.rb
Executable file
297
app/services/ticket_pdf_generator.rb
Executable file
@@ -0,0 +1,297 @@
|
||||
require "prawn"
|
||||
require "prawn/qrcode"
|
||||
require "rqrcode"
|
||||
|
||||
# Service de génération de billets PDF utilisant Prawn
|
||||
#
|
||||
# Génère des billets PDF simples et compacts avec codes QR pour la validation d'entrée
|
||||
# Design propre et minimaliste qui tient sur une seule page
|
||||
class TicketPdfGenerator
|
||||
# Suppress Prawn's internationalization warning for built-in fonts
|
||||
Prawn::Fonts::AFM.hide_m17n_warning = true
|
||||
attr_reader :ticket
|
||||
|
||||
def initialize(ticket)
|
||||
@ticket = ticket
|
||||
end
|
||||
|
||||
def generate
|
||||
Prawn::Document.new(page_size: [ 350, 600 ], margin: 20) do |pdf|
|
||||
# Header
|
||||
pdf.fill_color "2D1B69"
|
||||
pdf.font "Helvetica", style: :bold, size: 24
|
||||
pdf.text ENV.fetch("APP_NAME", "Aperonight"), align: :center
|
||||
pdf.move_down 10
|
||||
|
||||
# Event name
|
||||
pdf.fill_color "000000"
|
||||
pdf.font "Helvetica", style: :bold, size: 18
|
||||
pdf.text ticket.event.name, align: :center
|
||||
pdf.move_down 10
|
||||
|
||||
# Ticket info box
|
||||
pdf.stroke_color "E5E7EB"
|
||||
pdf.fill_color "F9FAFB"
|
||||
pdf.rounded_rectangle [ 0, pdf.cursor ], 310, 150, 10
|
||||
pdf.fill_and_stroke
|
||||
|
||||
pdf.move_down 10
|
||||
pdf.fill_color "000000"
|
||||
pdf.font "Helvetica", size: 12
|
||||
|
||||
# Customer name
|
||||
pdf.indent 10 do
|
||||
pdf.text "Titulaire du billet :", style: :bold
|
||||
pdf.text "#{ticket.first_name} #{ticket.last_name}"
|
||||
end
|
||||
pdf.move_down 8
|
||||
|
||||
# Ticket details
|
||||
pdf.indent 10 do
|
||||
pdf.text "Type de billet :", style: :bold
|
||||
pdf.text ticket.ticket_type.name
|
||||
end
|
||||
pdf.move_down 8
|
||||
|
||||
pdf.indent 10 do
|
||||
pdf.text "Prix :", style: :bold
|
||||
pdf.text "#{ticket.price_euros} €"
|
||||
end
|
||||
pdf.move_down 8
|
||||
|
||||
pdf.indent 10 do
|
||||
pdf.text "Date et heure :", style: :bold
|
||||
pdf.text ticket.event.start_time.strftime("%d %B %Y à %H:%M")
|
||||
end
|
||||
pdf.move_down 20
|
||||
|
||||
# Informations sur le lieu
|
||||
pdf.fill_color "374151"
|
||||
pdf.font "Helvetica", style: :bold, size: 14
|
||||
pdf.text "Informations sur le lieu"
|
||||
pdf.move_down 8
|
||||
|
||||
pdf.font "Helvetica", size: 11
|
||||
pdf.text ticket.event.venue_name, style: :bold
|
||||
pdf.text ticket.event.venue_address
|
||||
pdf.move_down 20
|
||||
|
||||
# Code QR
|
||||
pdf.fill_color "000000"
|
||||
pdf.font "Helvetica", style: :bold, size: 14
|
||||
pdf.text "Code QR", align: :center
|
||||
pdf.move_down 10
|
||||
|
||||
# Ensure all required data is present before generating QR code
|
||||
if ticket.qr_code.blank?
|
||||
raise "Ticket QR code is missing"
|
||||
end
|
||||
|
||||
# Build QR code data with safe association loading
|
||||
qr_code_data = build_qr_code_data(ticket)
|
||||
|
||||
# Validate QR code data before creating QR code
|
||||
if qr_code_data.blank? || qr_code_data == "{}"
|
||||
Rails.logger.error "QR code data is empty: ticket_id=#{ticket.id}, qr_code=#{ticket.qr_code}, event_id=#{ticket.ticket_type&.event_id}, user_id=#{ticket.order&.user_id}"
|
||||
raise "QR code data is empty or invalid"
|
||||
end
|
||||
|
||||
# Ensure qr_code_data is a proper string for QR code generation
|
||||
unless qr_code_data.is_a?(String) && qr_code_data.length > 2
|
||||
Rails.logger.error "QR code data is not a valid string: #{qr_code_data.inspect} (class: #{qr_code_data.class})"
|
||||
raise "QR code data must be a valid string"
|
||||
end
|
||||
|
||||
# Generate QR code - prawn-qrcode expects the data string directly
|
||||
pdf.print_qr_code(qr_code_data, extent: 120, align: :center)
|
||||
|
||||
pdf.move_down 15
|
||||
|
||||
# QR code text
|
||||
pdf.font "Helvetica", size: 8
|
||||
pdf.fill_color "6B7280"
|
||||
pdf.text "#{ticket.qr_code}", align: :center
|
||||
|
||||
|
||||
# Ticket ID
|
||||
pdf.font "Helvetica", size: 8
|
||||
pdf.fill_color "6B7280"
|
||||
pdf.text "ID du billet : #{ticket.id}", align: :center
|
||||
|
||||
# Footer
|
||||
pdf.move_down 30
|
||||
pdf.stroke_color "E5E7EB"
|
||||
pdf.horizontal_line 0, 310
|
||||
pdf.move_down 6
|
||||
|
||||
pdf.font "Helvetica", size: 8
|
||||
pdf.fill_color "6B7280"
|
||||
pdf.text "Ce billet est valable pour une seule entrée.", align: :center
|
||||
pdf.text "Présentez ce billet à l'entrée du lieu.", align: :center
|
||||
pdf.move_down 5
|
||||
pdf.text "Généré le #{Time.current.strftime('%d %B %Y à %H:%M')}", align: :center
|
||||
end.render
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_simple_header(pdf)
|
||||
# Nom de la marque
|
||||
pdf.fill_color "6366F1"
|
||||
pdf.font "Helvetica", style: :bold, size: 24
|
||||
pdf.text "AperoNight", align: :center
|
||||
|
||||
pdf.move_down 5
|
||||
pdf.font "Helvetica", size: 10
|
||||
pdf.fill_color "64748B"
|
||||
pdf.text "Billet d'entree", align: :center
|
||||
|
||||
pdf.move_down 20
|
||||
|
||||
# Simple divider line
|
||||
pdf.stroke_color "E5E7EB"
|
||||
pdf.horizontal_line 0, pdf.bounds.width
|
||||
pdf.move_down 20
|
||||
end
|
||||
|
||||
def create_ticket_info(pdf)
|
||||
# Nom de l'événement - proéminent
|
||||
pdf.fill_color "1F2937"
|
||||
pdf.font "Helvetica", style: :bold, size: 18
|
||||
pdf.text ticket.event.name, align: :center
|
||||
pdf.move_down 15
|
||||
|
||||
# Two-column layout for ticket details
|
||||
pdf.bounding_box([ 0, pdf.cursor ], width: pdf.bounds.width, height: 120) do
|
||||
# Left column
|
||||
pdf.bounding_box([ 0, pdf.cursor ], width: pdf.bounds.width / 2 - 20, height: 120) do
|
||||
create_info_item(pdf, "Date", ticket.event.start_time.strftime("%d %B %Y"))
|
||||
create_info_item(pdf, "Heure", ticket.event.start_time.strftime("%H:%M"))
|
||||
create_info_item(pdf, "Lieu", ticket.event.venue_name)
|
||||
end
|
||||
|
||||
# Right column
|
||||
pdf.bounding_box([ pdf.bounds.width / 2 + 20, pdf.cursor ], width: pdf.bounds.width / 2 - 20, height: 120) do
|
||||
create_info_item(pdf, "Type", ticket.ticket_type.name)
|
||||
create_info_item(pdf, "Prix", "#{sprintf('%.2f', ticket.price_euros)} €")
|
||||
create_info_item(pdf, "Titulaire", "#{ticket.first_name} #{ticket.last_name}")
|
||||
end
|
||||
end
|
||||
|
||||
pdf.move_down 30
|
||||
end
|
||||
|
||||
def create_info_item(pdf, label, value)
|
||||
pdf.font "Helvetica", style: :bold, size: 9
|
||||
pdf.fill_color "64748B"
|
||||
pdf.text label.upcase
|
||||
|
||||
pdf.move_down 2
|
||||
pdf.font "Helvetica", size: 11
|
||||
pdf.fill_color "1F2937"
|
||||
pdf.text value
|
||||
pdf.move_down 12
|
||||
end
|
||||
|
||||
def create_qr_section(pdf)
|
||||
# Center the QR code horizontally
|
||||
qr_size = 120
|
||||
x_position = (pdf.bounds.width - qr_size) / 2
|
||||
|
||||
pdf.bounding_box([ x_position, pdf.cursor ], width: qr_size, height: qr_size + 40) do
|
||||
# QR Code title
|
||||
pdf.font "Helvetica", style: :bold, size: 12
|
||||
pdf.fill_color "1F2937"
|
||||
pdf.text "Code d'entree", align: :center
|
||||
pdf.move_down 10
|
||||
|
||||
# Generate QR code
|
||||
generate_simple_qr_code(pdf, qr_size)
|
||||
|
||||
pdf.move_down 10
|
||||
|
||||
# QR code ID
|
||||
pdf.font "Helvetica", size: 8
|
||||
pdf.fill_color "64748B"
|
||||
pdf.text "ID: #{ticket.qr_code[0..15]}...", align: :center
|
||||
end
|
||||
|
||||
pdf.move_down 40
|
||||
end
|
||||
|
||||
def generate_simple_qr_code(pdf, size)
|
||||
# Ensure all required data is present before generating QR code
|
||||
if ticket.qr_code.blank?
|
||||
raise "Ticket QR code is missing"
|
||||
end
|
||||
|
||||
# Build QR code data with safe association loading
|
||||
qr_code_data = build_qr_code_data(ticket)
|
||||
|
||||
# Validate QR code data before creating QR code
|
||||
if qr_code_data.blank? || qr_code_data == "{}"
|
||||
Rails.logger.error "QR code data is empty: ticket_id=#{ticket.id}, qr_code=#{ticket.qr_code}, event_id=#{ticket.ticket_type&.event_id}, user_id=#{ticket.order&.user_id}"
|
||||
raise "QR code data is empty or invalid"
|
||||
end
|
||||
|
||||
# Ensure qr_code_data is a proper string for QR code generation
|
||||
unless qr_code_data.is_a?(String) && qr_code_data.length > 2
|
||||
Rails.logger.error "QR code data is not a valid string: #{qr_code_data.inspect} (class: #{qr_code_data.class})"
|
||||
raise "QR code data must be a valid string"
|
||||
end
|
||||
|
||||
# Generate QR code
|
||||
pdf.print_qr_code(qr_code_data, extent: size, align: :center)
|
||||
end
|
||||
|
||||
def create_simple_footer(pdf)
|
||||
# Security notice
|
||||
pdf.font "Helvetica", size: 8
|
||||
pdf.fill_color "64748B"
|
||||
pdf.text "Ce billet est valable pour une seule entree.", align: :center
|
||||
pdf.text "Presentez ce code QR a l'entree de l'evenement.", align: :center
|
||||
|
||||
pdf.move_down 10
|
||||
|
||||
# Divider line
|
||||
pdf.stroke_color "E5E7EB"
|
||||
pdf.horizontal_line 0, pdf.bounds.width
|
||||
pdf.move_down 5
|
||||
|
||||
# Generation timestamp
|
||||
pdf.font "Helvetica", size: 7
|
||||
pdf.fill_color "9CA3AF"
|
||||
timestamp = "Genere le #{Time.current.strftime('%d/%m/%Y a %H:%M')}"
|
||||
pdf.text timestamp, align: :center
|
||||
end
|
||||
|
||||
def build_qr_code_data(ticket)
|
||||
# Try multiple approaches to get valid QR code data
|
||||
begin
|
||||
# Primary approach: full JSON with all data
|
||||
data = {
|
||||
ticket_id: ticket.id,
|
||||
qr_code: ticket.qr_code,
|
||||
event_id: ticket.ticket_type&.event_id,
|
||||
user_id: ticket.order&.user_id
|
||||
}.compact
|
||||
|
||||
# Ensure we have the minimum required data
|
||||
if data[:ticket_id] && data[:qr_code]
|
||||
return data.to_json
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.warn "Failed to build complex QR data: #{e.message}"
|
||||
end
|
||||
|
||||
# Fallback approach: just use the ticket's QR code string
|
||||
begin
|
||||
return ticket.qr_code.to_s if ticket.qr_code.present?
|
||||
rescue StandardError => e
|
||||
Rails.logger.warn "Failed to use ticket QR code: #{e.message}"
|
||||
end
|
||||
|
||||
# Final fallback: simple ticket identifier
|
||||
"TICKET-#{ticket.id}"
|
||||
end
|
||||
end
|
||||
31
app/views/components/_breadcrumb.html.erb
Normal file
31
app/views/components/_breadcrumb.html.erb
Normal file
@@ -0,0 +1,31 @@
|
||||
<%# Dynamic breadcrumb navigation component %>
|
||||
<%# Usage: render 'components/breadcrumb', crumbs: [ %>
|
||||
<%# { name: 'Home', path: root_path }, %>
|
||||
<%# { name: 'Events', path: events_path }, %>
|
||||
<%# { name: 'Current Event', path: nil } %>
|
||||
<%# ] %>
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="inline-flex items-center gap-2 bg-white px-4 py-3 rounded-xl shadow-sm border border-gray-100 mb-8" aria-label="Breadcrumb">
|
||||
<% crumbs.each_with_index do |crumb, index| %>
|
||||
<% if crumb[:path].present? %>
|
||||
<%# Crumb with link %>
|
||||
<%= link_to crumb[:path], class: "inline-flex items-center text-sm font-medium text-gray-700 hover:text-primary-600 transition-colors duration-200" do %>
|
||||
<% if index == 0 %>
|
||||
<i data-lucide="home" class="w-4 h-4 mr-2"></i>
|
||||
<% end %>
|
||||
<%= crumb[:name] %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%# Current page (no link) %>
|
||||
<span class="text-sm font-medium text-primary-600 truncate max-w-xs" aria-current="page">
|
||||
<%= crumb[:name] %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<%# Separator (except for the last item) %>
|
||||
<% if index < crumbs.length - 1 %>
|
||||
<i data-lucide="chevron-right" class="w-4 h-4 text-gray-400"></i>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</nav>
|
||||
17
app/views/components/_delete_account.html.erb
Normal file
17
app/views/components/_delete_account.html.erb
Normal file
@@ -0,0 +1,17 @@
|
||||
<!-- Delete Account Section -->
|
||||
<div class="bg-white py-8 px-6 shadow-xl rounded-2xl">
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-4">Supprimer mon compte</h3>
|
||||
<p class="text-gray-600 mb-6">
|
||||
Vous êtes certain de vouloir supprimer votre compte ? Cette action est irréversible.
|
||||
</p>
|
||||
<%= button_to registration_path(resource_name),
|
||||
data: {
|
||||
confirm: "Êtes-vous certain ?",
|
||||
turbo_confirm: "Êtes-vous certain ?"
|
||||
},
|
||||
method: :delete,
|
||||
class: "group relative w-full flex justify-center items-center py-3 px-4 border border-red-300 text-sm font-semibold rounded-xl text-red-700 bg-red-50 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-all duration-200" do %>
|
||||
<i data-lucide="trash-2" class="w-4 h-4 mr-2"></i>
|
||||
Supprimer mon compte
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -1,40 +1,92 @@
|
||||
<div class="grid gap-6 mb-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div class="grid gap-8 mb-8 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div>
|
||||
<h3 class="font-bold text-lg text-white mb-3">À propos</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Je suis organisateur</a></li>
|
||||
<%# <li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Pour les artistes</a></li> %>
|
||||
<%# <li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Kit presse</a></li> %>
|
||||
<%# <li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Artistes</a></li> %>
|
||||
<h3 class="font-bold text-lg text-white mb-4 flex items-center">
|
||||
<i data-lucide="info" class="w-5 h-5 mr-2 text-yellow-400"></i>
|
||||
À propos
|
||||
</h3>
|
||||
<ul class="space-y-3">
|
||||
<li><a href="#" class="text-gray-400 text-sm hover:text-yellow-400 transition-colors duration-300 flex items-center">
|
||||
<i data-lucide="briefcase" class="w-4 h-4 mr-2"></i>
|
||||
Je suis organisateur
|
||||
</a></li>
|
||||
<li><a href="#" class="text-gray-400 text-sm hover:text-yellow-400 transition-colors duration-300 flex items-center">
|
||||
<i data-lucide="users" class="w-4 h-4 mr-2"></i>
|
||||
Communauté
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-lg text-white mb-3">Villes</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Paris</a></li>
|
||||
<%# <li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Lyon</a></li> %>
|
||||
<%# <li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Marseille / Aix-en-Provence</a></li> %>
|
||||
<%# <li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Toulouse</a></li> %>
|
||||
<h3 class="font-bold text-lg text-white mb-4 flex items-center">
|
||||
<i data-lucide="map-pin" class="w-5 h-5 mr-2 text-yellow-400"></i>
|
||||
Villes
|
||||
</h3>
|
||||
<ul class="space-y-3">
|
||||
<li><a href="#" class="text-gray-400 text-sm hover:text-yellow-400 transition-colors duration-300 flex items-center">
|
||||
<i data-lucide="map" class="w-4 h-4 mr-2"></i>
|
||||
Paris
|
||||
</a></li>
|
||||
<li><a href="#" class="text-gray-400 text-sm hover:text-yellow-400 transition-colors duration-300 flex items-center">
|
||||
<i data-lucide="map" class="w-4 h-4 mr-2"></i>
|
||||
Lyon (bientôt)
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<%# <div>
|
||||
<h3 class="font-bold text-lg text-white mb-3">Organisateurs</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Help Center</a></li>
|
||||
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Contact Us</a></li>
|
||||
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Safety Guidelines</a></li>
|
||||
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Cancellation Policy</a></li>
|
||||
</ul>
|
||||
</div> %>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-lg text-white mb-3">Support</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Aide</a></li>
|
||||
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Nous contacter</a></li>
|
||||
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Signaler un contenu inapproprié</a></li>
|
||||
<h3 class="font-bold text-lg text-white mb-4 flex items-center">
|
||||
<i data-lucide="calendar" class="w-5 h-5 mr-2 text-yellow-400"></i>
|
||||
Événements
|
||||
</h3>
|
||||
<ul class="space-y-3">
|
||||
<li><a href="<%= events_path %>" class="text-gray-400 text-sm hover:text-yellow-400 transition-colors duration-300 flex items-center">
|
||||
<i data-lucide="glass-water" class="w-4 h-4 mr-2"></i>
|
||||
Afterworks
|
||||
</a></li>
|
||||
<li><a href="#" class="text-gray-400 text-sm hover:text-yellow-400 transition-colors duration-300 flex items-center">
|
||||
<i data-lucide="music" class="w-4 h-4 mr-2"></i>
|
||||
Concerts
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-lg text-white mb-4 flex items-center">
|
||||
<i data-lucide="help-circle" class="w-5 h-5 mr-2 text-yellow-400"></i>
|
||||
Support
|
||||
</h3>
|
||||
<ul class="space-y-3">
|
||||
<li><a href="#" class="text-gray-400 text-sm hover:text-yellow-400 transition-colors duration-300 flex items-center">
|
||||
<i data-lucide="life-buoy" class="w-4 h-4 mr-2"></i>
|
||||
Aide
|
||||
</a></li>
|
||||
<li><a href="#" class="text-gray-400 text-sm hover:text-yellow-400 transition-colors duration-300 flex items-center">
|
||||
<i data-lucide="mail" class="w-4 h-4 mr-2"></i>
|
||||
Nous contacter
|
||||
</a></li>
|
||||
<li><a href="#" class="text-gray-400 text-sm hover:text-yellow-400 transition-colors duration-300 flex items-center">
|
||||
<i data-lucide="flag" class="w-4 h-4 mr-2"></i>
|
||||
Signaler un contenu
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-neutral-700 pt-4 text-center text-neutral-400 text-sm">
|
||||
<p>© 2025 Aperonight. All rights reserved. • <a href="#" class="text-accent-400 hover:text-accent-300 transition-colors">Privacy Policy</a> • <a href="#" class="text-accent-400 hover:text-accent-300 transition-colors">Terms of Service</a></p>
|
||||
|
||||
<div class="border-t border-gray-700 pt-6 text-center">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-center">
|
||||
<p class="text-gray-400 text-sm mb-4 sm:mb-0">
|
||||
© 2025 Aperonight. Tous droits réservés.
|
||||
</p>
|
||||
<div class="flex space-x-6">
|
||||
<a href="#" class="text-gray-400 hover:text-yellow-400 transition-colors text-sm">
|
||||
Politique de confidentialité
|
||||
</a>
|
||||
<a href="#" class="text-gray-400 hover:text-yellow-400 transition-colors text-sm">
|
||||
Conditions d'utilisation
|
||||
</a>
|
||||
<a href="#" class="text-gray-400 hover:text-yellow-400 transition-colors text-sm">
|
||||
Mentions légales
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,22 +1,40 @@
|
||||
<header class="bg-neutral-800 border-b border-neutral-700">
|
||||
<header class="bg-white shadow-lg border-b border-gray-200 sticky top-0 z-50">
|
||||
<nav data-controller="header" class="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center h-16 justify-between">
|
||||
<!-- Logo -->
|
||||
<div class="flex-shrink-0 flex items-center">
|
||||
<%= link_to Rails.application.config.app_name, current_user ? "/dashboard" : "/",
|
||||
class: "text-xl font-bold text-white" %>
|
||||
<%= link_to Rails.application.config.app_name, "/",
|
||||
class: "text-2xl font-display font-bold text-gray-900 hover:text-brand-primary transition-colors" %>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden sm:flex items-center space-x-6 w-full justify-start">
|
||||
<%= link_to "Afterworks", events_path,
|
||||
class: "mx-4 text-gray-100 hover:text-purple-200 py-2 rounded-md text-sm font-medium transition-colors duration-200" %>
|
||||
<div class="hidden sm:flex items-center space-x-8 w-full justify-center">
|
||||
<%= link_to events_path,
|
||||
class: "text-gray-700 hover:text-brand-primary py-2 text-sm font-medium transition-colors duration-200 relative after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-0 hover:after:w-full after:bg-brand-primary after:transition-all after:duration-200" do %>
|
||||
Événements & Afterworks
|
||||
<% end %>
|
||||
|
||||
<%= link_to "Évenements", "#",
|
||||
class: "mx-4 text-gray-100 hover:text-purple-200 py-2 rounded-md text-sm font-medium transition-colors duration-200" %>
|
||||
<%= link_to dashboard_path,
|
||||
class: "text-gray-700 hover:text-brand-primary py-2 text-sm font-medium transition-colors duration-200 relative after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-0 hover:after:w-full after:bg-brand-primary after:transition-all after:duration-200" do %>
|
||||
Tableau de bord
|
||||
<% end %>
|
||||
|
||||
<%= link_to "Concerts", "#",
|
||||
class: "mx-4 text-gray-100 hover:text-purple-200 py-2 rounded-md text-sm font-medium transition-colors duration-200" %>
|
||||
<% if user_signed_in? && current_user.promoter? %>
|
||||
<%= link_to new_promoter_event_path,
|
||||
class: "text-gray-700 hover:text-brand-primary py-2 text-sm font-medium transition-colors duration-200 relative after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-0 hover:after:w-full after:bg-brand-primary after:transition-all after:duration-200" do %>
|
||||
Créer un événement
|
||||
<% end %>
|
||||
|
||||
<%= link_to promoter_events_path,
|
||||
class: "text-gray-700 hover:text-brand-primary py-2 text-sm font-medium transition-colors duration-200 relative after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-0 hover:after:w-full after:bg-brand-primary after:transition-all after:duration-200" do %>
|
||||
Mes événements
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<!-- <%= link_to "#",
|
||||
class: "text-gray-700 hover:text-brand-primary py-2 text-sm font-medium transition-colors duration-200 relative after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-0 hover:after:w-full after:bg-brand-primary after:transition-all after:duration-200" do %>
|
||||
Concerts
|
||||
<% end %> -->
|
||||
</div>
|
||||
|
||||
<!-- Authentication -->
|
||||
@@ -24,82 +42,147 @@
|
||||
<% if user_signed_in? %>
|
||||
<div class="relative" data-header-target="userMenuButton">
|
||||
<button data-action="click->header#toggleUserMenu"
|
||||
class="bg-purple-700 text-white border border-purple-800 font-medium py-2 px-4 rounded-lg hover:bg-purple-800 transition-colors duration-200 flex items-center space-x-2">
|
||||
class="bg-gray-900 text-white font-medium py-2 px-4 rounded-full hover:bg-gray-800 transition-colors duration-200 flex items-center space-x-2">
|
||||
<span><%= current_user.email.length > 20 ? current_user.email[0,20] + "..." : current_user.email %></span>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<i data-lucide="chevron-down" class="w-4 h-4"></i>
|
||||
</button>
|
||||
|
||||
<!-- User Dropdown Menu -->
|
||||
<div data-header-target="userMenu" class="hidden absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50">
|
||||
<div class="px-4 py-2 text-sm text-gray-900 border-b border-gray-100">
|
||||
<div class="font-medium"><%= current_user.first_name || current_user.email %></div>
|
||||
<div data-header-target="userMenu" class="hidden absolute right-0 mt-2 w-56 bg-white rounded-2xl shadow-xl border border-gray-100 py-2 z-50">
|
||||
<div class="px-4 py-3 text-sm text-gray-900 border-b border-gray-100">
|
||||
<div class="font-semibold"><%= current_user.first_name || current_user.email %></div>
|
||||
<div class="text-gray-500"><%= current_user.email %></div>
|
||||
</div>
|
||||
<%= link_to "Profile", edit_user_registration_path,
|
||||
class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-200" %>
|
||||
<%= link_to "Reservations", "#",
|
||||
class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-200" %>
|
||||
<%= link_to "#",
|
||||
class: "flex items-center px-4 py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-200" do %>
|
||||
<i data-lucide="calendar" class="w-4 h-4 mr-3"></i>
|
||||
Réservations
|
||||
<% end %>
|
||||
<%= link_to settings_path,
|
||||
class: "flex items-center px-4 py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-200" do %>
|
||||
<i data-lucide="user" class="w-4 h-4 mr-3"></i>
|
||||
Profil
|
||||
<% end %>
|
||||
<%= link_to edit_user_registration_path,
|
||||
class: "flex items-center px-4 py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-200" do %>
|
||||
<i data-lucide="key" class="w-4 h-4 mr-3"></i>
|
||||
Sécurité
|
||||
<% end %>
|
||||
<div class="border-t border-gray-100">
|
||||
<%= link_to "Sign out", destroy_user_session_path,
|
||||
<%= link_to destroy_user_session_path,
|
||||
data: { controller: "logout", action: "click->logout#signOut", logout_url_value: destroy_user_session_path, login_url_value: new_user_session_path, turbo: false },
|
||||
class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-200" %>
|
||||
class: "flex items-center px-4 py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-200" do %>
|
||||
<i data-lucide="log-out" class="w-4 h-4 mr-3"></i>
|
||||
Déconnexion
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= link_to t("header.login"), new_user_session_path,
|
||||
class: "text-gray-100 hover:text-purple-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200" %>
|
||||
<%= link_to t("header.register"), new_user_registration_path,
|
||||
class: "bg-purple-600 text-white font-medium py-2 px-4 rounded-lg hover:bg-purple-700 transition-colors duration-200" %>
|
||||
<%= link_to new_user_session_path,
|
||||
class: "text-gray-700 hover:text-brand-primary px-6 py-2 rounded-full text-sm font-medium transition-colors duration-200 whitespace-nowrap" do %>
|
||||
Se connecter
|
||||
<% end %>
|
||||
<%= link_to new_user_registration_path,
|
||||
class: "bg-gray-900 text-white font-semibold py-2 px-8 rounded-full hover:bg-gray-800 transition-colors duration-200 shadow-lg hover:shadow-xl whitespace-nowrap" do %>
|
||||
S'inscrire
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu button -->
|
||||
<div class="flex-shrink-0 sm:hidden">
|
||||
<button data-action="click->header#toggleMobileMenu" data-header-target="mobileMenuButton" class="p-2 rounded-md text-neutral-300 hover:text-white hover:bg-purple-700">
|
||||
<svg data-menu-icon="open" class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg data-menu-icon="close" class="h-6 w-6 hidden" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<button data-action="click->header#toggleMobileMenu" data-header-target="mobileMenuButton" class="p-2 rounded-lg text-gray-600 hover:text-gray-900 hover:bg-gray-100">
|
||||
<i data-menu-icon="open" data-lucide="menu" class="w-6 h-6"></i>
|
||||
<i data-menu-icon="close" data-lucide="x" class="w-6 h-6 hidden"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<div data-header-target="mobileMenu" class="hidden sm:hidden">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1">
|
||||
<%= link_to t("header.parties"), events_path,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
|
||||
<%= link_to t("header.concerts"), "#",
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
|
||||
<div data-header-target="mobileMenu" class="hidden sm:hidden border-t border-gray-200">
|
||||
<div class="px-4 pt-4 pb-3 space-y-2">
|
||||
<%= link_to events_path,
|
||||
class: "flex items-center px-3 py-2 rounded-lg text-base font-medium text-gray-700 hover:text-brand-primary hover:bg-gray-50" do %>
|
||||
<i data-lucide="calendar" class="w-4 h-4 mr-3"></i>
|
||||
Événements & Afterworks
|
||||
<% end %>
|
||||
|
||||
<%= link_to dashboard_path,
|
||||
class: "flex items-center px-3 py-2 rounded-lg text-base font-medium text-gray-700 hover:text-brand-primary hover:bg-gray-50" do %>
|
||||
<i data-lucide="bar-chart-3" class="w-4 h-4 mr-3"></i>
|
||||
Tableau de bord
|
||||
<% end %>
|
||||
|
||||
<% if user_signed_in? && current_user.promoter? %>
|
||||
<%= link_to new_promoter_event_path,
|
||||
class: "flex items-center px-3 py-2 rounded-lg text-base font-medium text-gray-700 hover:text-brand-primary hover:bg-gray-50" do %>
|
||||
<i data-lucide="plus-circle" class="w-4 h-4 mr-3"></i>
|
||||
Créer un événement
|
||||
<% end %>
|
||||
|
||||
<%= link_to promoter_events_path,
|
||||
class: "flex items-center px-3 py-2 rounded-lg text-base font-medium text-gray-700 hover:text-brand-primary hover:bg-gray-50" do %>
|
||||
<i data-lucide="calendar-check" class="w-4 h-4 mr-3"></i>
|
||||
Mes événements
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<!-- <%= link_to events_path,
|
||||
class: "flex items-center px-3 py-2 rounded-lg text-base font-medium text-gray-700 hover:text-brand-primary hover:bg-gray-50" do %>
|
||||
<i data-lucide="glass-water" class="w-4 h-4 mr-3"></i>
|
||||
Afterworks
|
||||
<% end %> -->
|
||||
<!-- <%= link_to "#",
|
||||
class: "flex items-center px-3 py-2 rounded-lg text-base font-medium text-gray-700 hover:text-brand-primary hover:bg-gray-50" do %>
|
||||
<i data-lucide="music" class="w-4 h-4 mr-3"></i>
|
||||
Concerts
|
||||
<% end %> -->
|
||||
</div>
|
||||
|
||||
<div class="pt-4 pb-3 border-t border-gray-700">
|
||||
<div class="pt-4 pb-4 border-t border-gray-200">
|
||||
<% if user_signed_in? %>
|
||||
<div class="px-4 mb-3">
|
||||
<div class="text-base font-medium text-white">
|
||||
<div class="text-base font-semibold text-gray-900">
|
||||
<%= current_user.first_name || current_user.email %>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500"><%= current_user.email %></div>
|
||||
</div>
|
||||
<div class="px-2 space-y-1">
|
||||
<%= link_to t("header.profile"), edit_user_registration_path,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
|
||||
<%= link_to t("header.reservations"), "#",
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
|
||||
<%= link_to t("header.logout"), destroy_user_session_path,
|
||||
<div class="px-4 space-y-2">
|
||||
<%= link_to "#",
|
||||
class: "flex items-center px-3 py-2 rounded-lg text-base font-medium text-gray-700 hover:text-brand-primary hover:bg-gray-50" do %>
|
||||
<i data-lucide="calendar" class="w-4 h-4 mr-3"></i>
|
||||
Réservations
|
||||
<% end %>
|
||||
<%= link_to settings_path,
|
||||
class: "flex items-center px-3 py-2 rounded-lg text-base font-medium text-gray-700 hover:text-brand-primary hover:bg-gray-50" do %>
|
||||
<i data-lucide="user" class="w-4 h-4 mr-3"></i>
|
||||
Profil
|
||||
<% end %>
|
||||
<%= link_to edit_user_registration_path,
|
||||
class: "flex items-center px-3 py-2 rounded-lg text-base font-medium text-gray-700 hover:text-brand-primary hover:bg-gray-50" do %>
|
||||
<i data-lucide="key" class="w-4 h-4 mr-3"></i>
|
||||
Sécurité
|
||||
<% end %>
|
||||
<%= link_to destroy_user_session_path,
|
||||
data: { controller: "logout", action: "click->logout#signOut", logout_url_value: destroy_user_session_path, login_url_value: new_user_session_path, turbo: false },
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
|
||||
class: "flex items-center px-3 py-2 rounded-lg text-base font-medium text-gray-700 hover:text-brand-primary hover:bg-gray-50" do %>
|
||||
<i data-lucide="log-out" class="w-4 h-4 mr-3"></i>
|
||||
Déconnexion
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="px-2 space-y-1">
|
||||
<%= link_to t("header.login"), new_user_session_path,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
|
||||
<%= link_to t("header.register"), new_user_registration_path,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium bg-purple-600 text-white hover:bg-purple-700" %>
|
||||
<div class="px-4 space-y-2">
|
||||
<%= link_to new_user_session_path,
|
||||
class: "flex items-center justify-center px-3 py-2 rounded-lg text-base font-medium text-gray-700 hover:text-brand-primary hover:bg-gray-50" do %>
|
||||
<i data-lucide="log-in" class="w-4 h-4 mr-3"></i>
|
||||
Se connecter
|
||||
<% end %>
|
||||
<%= link_to new_user_registration_path,
|
||||
class: "flex items-center justify-center px-3 py-2 rounded-lg text-base font-semibold bg-gray-900 text-white hover:bg-gray-800" do %>
|
||||
<i data-lucide="user-plus" class="w-4 h-4 mr-3"></i>
|
||||
S'inscrire
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -16,17 +16,13 @@
|
||||
<div class="<%= 'order-2 sm:order-1' unless sold_out %>">
|
||||
<% if sold_out %>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||
<svg class="-ml-0.5 mr-1 h-2 w-2 text-red-400" fill="currentColor" viewBox="0 0 8 8">
|
||||
<circle cx="4" cy="4" r="3" />
|
||||
</svg>
|
||||
<i data-lucide="x" class="w-2 h-2 mr-1 text-red-400"></i>
|
||||
Épuisé
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
<svg class="-ml-0.5 mr-1 h-2 w-2 text-green-400" fill="currentColor" viewBox="0 0 8 8">
|
||||
<circle cx="4" cy="4" r="3" />
|
||||
</svg>
|
||||
<%= remaining %> disponibles
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-gray-50 text-gray-600 border border-gray-200">
|
||||
<i data-lucide="ticket" class="w-3 h-3 mr-1 text-green-500"></i>
|
||||
<%= remaining %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -59,9 +55,7 @@
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-sm text-gray-500 font-medium order-1 sm:order-2">
|
||||
<svg class="w-5 h-5 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
||||
</svg>
|
||||
<i data-lucide="lock" class="w-5 h-5 inline-block mr-1"></i>
|
||||
Indisponible
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -1,34 +1,52 @@
|
||||
<div class="flex items-center justify-center bg-neutral-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="min-h-screen max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
<!-- Header -->
|
||||
<div class="text-center">
|
||||
<%= link_to "/" do %>
|
||||
<img class="mx-auto h-12 w-auto" src="/icon.svg" alt="Aperonight" />
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-purple-600 to-blue-600 rounded-2xl mb-6">
|
||||
<i data-lucide="calendar" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<% end %>
|
||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-neutral-900">
|
||||
<%= t('devise.confirmations.new.title') %>
|
||||
</h2>
|
||||
<p class="mt-2 text-center text-sm text-neutral-600">
|
||||
<h2 class="text-3xl font-bold text-gray-900"><%= t('devise.confirmations.new.title') %></h2>
|
||||
<p class="mt-2 text-gray-600">
|
||||
<%= t('devise.confirmations.new.description') %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: "mt-8 space-y-6" }) do |f| %>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="bg-white py-8 px-6 shadow-xl rounded-2xl">
|
||||
<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: "space-y-6" }) do |f| %>
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<%= f.label :email, class: "block text-sm font-medium text-neutral-700" %>
|
||||
<div class="mt-1">
|
||||
<%= f.label :email, class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i data-lucide="mail" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email),
|
||||
class: "appearance-none block w-full px-3 py-2 border border-neutral-300 rounded-md shadow-sm placeholder-neutral-400 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm",
|
||||
placeholder: "Email" %>
|
||||
placeholder: "votre@email.com",
|
||||
class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.submit t('devise.confirmations.new.submit'),
|
||||
class: "group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-neutral-50 focus:ring-purple-500" %>
|
||||
<div class="pt-4">
|
||||
<%= f.button type: "submit", class: "group relative w-full flex justify-center items-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white bg-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" do %>
|
||||
<i data-lucide="send" class="w-4 h-4 mr-2"></i>
|
||||
<%= t('devise.confirmations.new.submit') %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
<!-- Additional Links -->
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
Vous vous souvenez de votre mot de passe ?
|
||||
<a href="<%= new_user_session_path %>" class="font-semibold text-purple-600 hover:text-purple-500 transition-colors">Se connecter</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,43 +1,69 @@
|
||||
<div class="flex items-center justify-center bg-neutral-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="min-h-screen max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
<!-- Header -->
|
||||
<div class="text-center">
|
||||
<%= link_to "/" do %>
|
||||
<img class="mx-auto h-12 w-auto" src="/icon.svg" alt="Aperonight" />
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-purple-600 to-blue-600 rounded-2xl mb-6">
|
||||
<i data-lucide="calendar" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<% end %>
|
||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-neutral-900">
|
||||
<%= t('devise.passwords.edit.title') %>
|
||||
</h2>
|
||||
<p class="mt-2 text-center text-sm text-neutral-600">
|
||||
<h2 class="text-3xl font-bold text-gray-900"><%= t('devise.passwords.edit.title') %></h2>
|
||||
<p class="mt-2 text-gray-600">
|
||||
<%= t('devise.passwords.edit.description') %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: "mt-8 space-y-6" }) do |f| %>
|
||||
<!-- Form -->
|
||||
<div class="bg-white py-8 px-6 shadow-xl rounded-2xl">
|
||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: "space-y-6" }) do |f| %>
|
||||
<%= f.hidden_field :reset_password_token %>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<%= f.label :password, t('devise.passwords.edit.new_password'), class: "block text-sm font-medium text-neutral-700" %>
|
||||
<%= f.label :password, t('devise.passwords.edit.new_password'), class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<% if @minimum_password_length %>
|
||||
<em class="text-sm text-neutral-500">(<%= t('devise.registrations.new.minimum_password_length', count: @minimum_password_length) %>)</em>
|
||||
<p class="text-xs text-gray-500 mb-2">(<%= t('devise.registrations.new.minimum_password_length', count: @minimum_password_length) %>)</p>
|
||||
<% end %>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i data-lucide="lock" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
<%= f.password_field :password, autofocus: true, autocomplete: "new-password",
|
||||
class: "mt-1 block w-full px-3 py-2 border border-neutral-300 rounded-md shadow-sm placeholder-neutral-400 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm" %>
|
||||
placeholder: "Votre nouveau mot de passe",
|
||||
class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :password_confirmation, t('devise.passwords.edit.confirm_new_password'), class: "block text-sm font-medium text-neutral-700" %>
|
||||
<%= f.label :password_confirmation, t('devise.passwords.edit.confirm_new_password'), class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i data-lucide="lock" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
<%= f.password_field :password_confirmation, autocomplete: "new-password",
|
||||
class: "mt-1 block w-full px-3 py-2 border border-neutral-300 rounded-md shadow-sm placeholder-neutral-400 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm" %>
|
||||
placeholder: "Confirmez votre nouveau mot de passe",
|
||||
class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit t('devise.passwords.edit.submit'),
|
||||
class: "group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-neutral-50 focus:ring-purple-500" %>
|
||||
<div class="pt-4">
|
||||
<%= f.button type: "submit", class: "group relative w-full flex justify-center items-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white bg-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" do %>
|
||||
<i data-lucide="refresh-ccw" class="w-4 h-4 mr-2"></i>
|
||||
<%= t('devise.passwords.edit.submit') %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
<!-- Additional Links -->
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
Vous vous souvenez de votre mot de passe ?
|
||||
<a href="<%= new_user_session_path %>" class="font-semibold text-purple-600 hover:text-purple-500 transition-colors">Se connecter</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,45 +1,51 @@
|
||||
<div class="flex items-center justify-center bg-neutral-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="min-h-screen max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
<!-- Header -->
|
||||
<div class="text-center">
|
||||
<%= link_to "/" do %>
|
||||
<img class="mx-auto h-12 w-auto" src="/icon.svg" alt="Aperonight" />
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-purple-600 to-blue-600 rounded-2xl mb-6">
|
||||
<i data-lucide="calendar" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<% end %>
|
||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-neutral-900">
|
||||
Mot de passe oublié ?
|
||||
</h2>
|
||||
<p class="mt-2 text-center text-sm text-neutral-600">
|
||||
<h2 class="text-3xl font-bold text-gray-900">Mot de passe oublié ?</h2>
|
||||
<p class="mt-2 text-gray-600">
|
||||
Entrez votre adresse email ci-dessous et nous vous enverrons un lien pour réinitialiser votre mot de passe.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: "mt-8 space-y-6" }) do |f| %>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="bg-white py-8 px-6 shadow-xl rounded-2xl">
|
||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: "space-y-6" }) do |f| %>
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<%= f.label :email, "Adresse Email", class: "block text-sm font-medium text-neutral-700" %>
|
||||
<div class="mt-1">
|
||||
<%= f.label :email, "Adresse email", class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i data-lucide="mail" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email",
|
||||
class: "appearance-none block w-full px-3 py-2 border border-neutral-300 rounded-md shadow-sm placeholder-neutral-400 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm",
|
||||
placeholder: t('devise.passwords.new.email_placeholder') %>
|
||||
placeholder: "votre@email.com",
|
||||
class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.submit "Envoyer le lien de réinitialisation",
|
||||
class: "group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-neutral-50 focus:ring-purple-500" %>
|
||||
<div class="pt-4">
|
||||
<%= f.button type: "submit", class: "group relative w-full flex justify-center items-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white bg-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" do %>
|
||||
<i data-lucide="send" class="w-4 h-4 mr-2"></i>
|
||||
Envoyer le lien de réinitialisation
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-neutral-300"></div>
|
||||
<!-- Additional Links -->
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
Vous vous souvenez de votre mot de passe ?
|
||||
<a href="<%= new_user_session_path %>" class="font-semibold text-purple-600 hover:text-purple-500 transition-colors">Se connecter</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 bg-neutral-50 text-neutral-600">Continuer avec</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,68 +1,111 @@
|
||||
<div class="min-h-screen flex items-center justify-center bg-neutral-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-2xl w-full space-y-8">
|
||||
<div>
|
||||
<div class="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-2xl mx-auto space-y-8">
|
||||
<!-- Breadcrumb -->
|
||||
<%= render 'components/breadcrumb', crumbs: [
|
||||
{ name: 'Accueil', path: root_path },
|
||||
{ name: 'Paramètres', path: settings_path },
|
||||
{ name: 'Modifier le compte', path: nil }
|
||||
] %>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center">
|
||||
<%= link_to "/" do %>
|
||||
<img class="mx-auto h-12 w-auto" src="/icon.svg" alt="Aperonight" />
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-purple-600 to-blue-600 rounded-2xl mb-6 mx-auto">
|
||||
<i data-lucide="calendar" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<% end %>
|
||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-neutral-900">
|
||||
Modifier votre compte
|
||||
</h2>
|
||||
<p class="mt-2 text-center text-sm text-neutral-600">
|
||||
Gérez vos informations et préférences
|
||||
<h2 class="text-3xl font-bold text-gray-900">Modifier vos informations de sécurité</h2>
|
||||
<p class="mt-2 text-gray-600">
|
||||
Gérez vos informations et préférences de sécurité
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: "mt-8 space-y-6" }) do |f| %>
|
||||
<!-- Profile Form -->
|
||||
<div class="bg-white py-8 px-6 shadow-xl rounded-2xl">
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-6">Informations du compte</h3>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: "space-y-6" }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<%= f.label :email, class: "block text-sm font-medium text-neutral-700" %>
|
||||
<%= f.label :email, class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i data-lucide="mail" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email",
|
||||
class: "mt-1 block w-full px-3 py-2 border border-neutral-300 rounded-md shadow-sm placeholder-neutral-400 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm" %>
|
||||
class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
|
||||
<div class="text-sm text-neutral-600">
|
||||
<div class="text-sm text-gray-600 bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<i data-lucide="alert-circle" class="w-5 h-5 text-yellow-500 inline mr-2"></i>
|
||||
En attente de confirmation pour : <%= resource.unconfirmed_email %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<%= f.label :password, "Nouveau mot de passe", class: "block text-sm font-medium text-neutral-700" %>
|
||||
<%= f.password_field :password, autocomplete: "new-password",
|
||||
class: "mt-1 block w-full px-3 py-2 border border-neutral-300 rounded-md shadow-sm placeholder-neutral-400 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm" %>
|
||||
<%= f.label :current_password, "Mot de passe actuel", class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i data-lucide="key" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :password_confirmation, "Confirmer le nouveau mot de passe", class: "block text-sm font-medium text-neutral-700" %>
|
||||
<%= f.password_field :password_confirmation, autocomplete: "new-password",
|
||||
class: "mt-1 block w-full px-3 py-2 border border-neutral-300 rounded-md shadow-sm placeholder-neutral-400 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :current_password, "Mot de passe actuel", class: "block text-sm font-medium text-neutral-700" %>
|
||||
<%= f.password_field :current_password, autocomplete: "current-password",
|
||||
class: "mt-1 block w-full px-3 py-2 border border-neutral-300 rounded-md shadow-sm placeholder-neutral-400 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm" %>
|
||||
placeholder: "Requis pour confirmer vos changements",
|
||||
class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors" %>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500">Requis pour confirmer vos changements</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<%= f.submit "Mettre à jour",
|
||||
class: "group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-neutral-50 focus:ring-purple-500" %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<%= f.label :password, "Nouveau mot de passe", class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i data-lucide="lock" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
<%= f.password_field :password, autocomplete: "new-password",
|
||||
placeholder: "Laisser vide si vous ne souhaitez pas le changer",
|
||||
class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors" %>
|
||||
</div>
|
||||
<% if @minimum_password_length %>
|
||||
<p class="mt-2 text-sm text-gray-500"><%= t('devise.registrations.new.minimum_password_length', count: @minimum_password_length) %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :password_confirmation, "Confirmer le nouveau mot de passe", class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i data-lucide="lock" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
<%= f.password_field :password_confirmation, autocomplete: "new-password",
|
||||
placeholder: "Confirmez votre nouveau mot de passe",
|
||||
class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<%= f.button type: "submit", class: "group relative w-full flex justify-center items-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white bg-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" do %>
|
||||
<i data-lucide="save" class="w-4 h-4 mr-2"></i>
|
||||
Mettre à jour
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<h3 class="text-center text-lg font-medium text-neutral-900"><%= t('devise.registrations.edit.delete_account') %></h3>
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-neutral-600">
|
||||
<%= t('devise.registrations.edit.unhappy') %> <%= button_to t('devise.registrations.edit.delete_account'), registration_path(resource_name),
|
||||
data: { confirm: t('devise.registrations.edit.confirm_delete'), turbo_confirm: t('devise.registrations.edit.confirm_delete') },
|
||||
method: :delete,
|
||||
class: "font-medium text-red-600 hover:text-red-500" %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= link_to t('devise.registrations.edit.back'), :back, class: "text-center block text-purple-600 hover:text-purple-500" %>
|
||||
<%# render "components/delete_account" %>
|
||||
|
||||
<!-- Back Link -->
|
||||
<div class="text-center">
|
||||
<%= link_to :back, class: "inline-flex items-center text-purple-600 hover:text-purple-500 transition-colors" do %>
|
||||
<i data-lucide="arrow-left" class="w-4 h-4 mr-2"></i>
|
||||
Retour
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,58 +1,91 @@
|
||||
<div class="flex items-center justify-center bg-neutral-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="min-h-screen max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
<!-- Header -->
|
||||
<div class="text-center">
|
||||
<%= link_to "/" do %>
|
||||
<img class="mx-auto h-12 w-auto" src="/icon.svg" alt="Aperonight" />
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-purple-600 to-blue-600 rounded-2xl mb-6">
|
||||
<i data-lucide="calendar" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<% end %>
|
||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-neutral-900">Créer un compte</h2>
|
||||
<p class="mt-2 text-center text-sm text-neutral-600">
|
||||
ou <a href="<%= new_user_session_path %>" class="font-medium text-purple-600 hover:text-purple-500">se connecter à votre compte</a>
|
||||
<h2 class="text-3xl font-bold text-gray-900">Créer un compte</h2>
|
||||
<p class="mt-2 text-gray-600">
|
||||
ou <a href="<%= new_user_session_path %>" class="font-semibold text-purple-600 hover:text-purple-500 transition-colors">se connecter à votre compte existant</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: "mt-8 space-y-6" }) do |f| %>
|
||||
<!-- Form -->
|
||||
<div class="bg-white py-8 px-6 shadow-xl rounded-2xl">
|
||||
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: "space-y-6" }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<%= f.label :email, "Adresse email", class: "block text-sm font-medium text-neutral-700" %>
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email",
|
||||
class: "mt-1 block w-full px-3 py-2 border border-neutral-300 rounded-md shadow-sm placeholder-neutral-500 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :password, "Mot de passe", class: "block text-sm font-medium text-neutral-700" %>
|
||||
<% if @minimum_password_length %>
|
||||
<em class="text-sm text-neutral-500">(<%= t('devise.registrations.new.minimum_password_length', count: @minimum_password_length) %>)</em>
|
||||
<% end %>
|
||||
<%= f.password_field :password, autocomplete: "new-password",
|
||||
class: "mt-1 block w-full px-3 py-2 border border-neutral-300 rounded-md shadow-sm placeholder-neutral-500 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :password_confirmation, "Confirmation du mot de passe", class: "block text-sm font-medium text-neutral-700" %>
|
||||
<%= f.password_field :password_confirmation, autocomplete: "new-password",
|
||||
class: "mt-1 block w-full px-3 py-2 border border-neutral-300 rounded-md shadow-sm placeholder-neutral-500 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="acthons">
|
||||
<%= f.submit "Créer un compte", class: "group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-neutral-50 focus:ring-purple-500" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-6">
|
||||
<%= f.label :email, "Adresse email", class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-neutral-300"></div>
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i data-lucide="mail" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 bg-neutral-50 text-neutral-600">Continuer avec</span>
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email",
|
||||
placeholder: "votre@email.com",
|
||||
class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<%= render "devise/shared/links" %>
|
||||
<div>
|
||||
<%= f.label :password, "Mot de passe", class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<% if @minimum_password_length %>
|
||||
<p class="text-xs text-gray-500 mb-2"><%= t('devise.registrations.new.minimum_password_length', count: @minimum_password_length) %></p>
|
||||
<% end %>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i data-lucide="lock" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
<%= f.password_field :password, autocomplete: "new-password",
|
||||
placeholder: "Votre mot de passe",
|
||||
class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :password_confirmation, "Confirmation du mot de passe", class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i data-lucide="lock" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
<%= f.password_field :password_confirmation, autocomplete: "new-password",
|
||||
placeholder: "Confirmez votre mot de passe",
|
||||
class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<%= f.button type: "submit", class: "group relative w-full flex justify-center items-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white bg-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" do %>
|
||||
<i data-lucide="user-plus" class="w-4 h-4 mr-2"></i>
|
||||
Créer un compte
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Additional Links -->
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
Vous avez déjà un compte?
|
||||
<a href="<%= new_user_session_path %>" class="font-semibold text-purple-600 hover:text-purple-500 transition-colors">Se connecter</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="text-center">
|
||||
<p class="text-xs text-gray-500">
|
||||
En créant un compte, vous acceptez nos
|
||||
<a href="#" class="text-purple-600 hover:text-purple-500">conditions d'utilisation</a>
|
||||
et notre
|
||||
<a href="#" class="text-purple-600 hover:text-purple-500">politique de confidentialité</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,64 +1,74 @@
|
||||
<div class="flex items-center justify-center bg-neutral-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="min-h-screen max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
<!-- Header -->
|
||||
<div class="text-center">
|
||||
<%= link_to "/" do %>
|
||||
<img class="mx-auto h-12 w-auto" src="/icon.svg" alt="Aperonight" />
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-purple-600 to-blue-600 rounded-2xl mb-6">
|
||||
<i data-lucide="calendar" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<% end %>
|
||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-neutral-900">
|
||||
Se connecter à votre compte
|
||||
</h2>
|
||||
<p class="mt-2 text-center text-sm text-neutral-600">
|
||||
ou
|
||||
<a href="<%= new_user_registration_path %>" class="font-medium text-purple-600 hover:text-purple-500">
|
||||
créer un compte
|
||||
</a>
|
||||
<h2 class="text-3xl font-bold text-gray-900">Connexion à votre compte</h2>
|
||||
<p class="mt-2 text-gray-600">
|
||||
ou <a href="<%= new_user_registration_path %>" class="font-semibold text-purple-600 hover:text-purple-500 transition-colors">créer un compte</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: "mt-8 space-y-6" }) do |f| %>
|
||||
|
||||
<div class="rounded-md shadow-sm -space-y-px">
|
||||
<div class="field">
|
||||
<%= f.label :email, "Email", class: "sr-only" %>
|
||||
<!-- Form -->
|
||||
<div class="bg-white py-8 px-6 shadow-xl rounded-2xl">
|
||||
<%= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: "space-y-6" }) do |f| %>
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<%= f.label :email, "Adresse email", class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i data-lucide="mail" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email",
|
||||
class: "appearance-none rounded-none relative block w-full px-3 py-2 border border-neutral-300 placeholder-neutral-500 text-neutral-900 bg-white rounded-t-md focus:outline-none focus:ring-purple-500 focus:border-purple-500 focus:z-10 sm:text-sm",
|
||||
placeholder: "Adresse email" %>
|
||||
placeholder: "votre@email.com",
|
||||
class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :password, "Mot de passe", class: "sr-only" %>
|
||||
<div>
|
||||
<%= f.label :password, "Mot de passe", class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i data-lucide="lock" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
<%= f.password_field :password, autocomplete: "current-password",
|
||||
class: "appearance-none rounded-none relative block w-full px-3 py-2 border border-neutral-300 placeholder-neutral-500 text-neutral-900 bg-white rounded-b-md focus:outline-none focus:ring-purple-500 focus:border-purple-500 focus:z-10 sm:text-sm",
|
||||
placeholder: "Mot de passe" %>
|
||||
placeholder: "Votre mot de passe",
|
||||
class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if devise_mapping.rememberable? %>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<%= f.check_box :remember_me, class: "h-4 w-4 text-purple-600 focus:ring-purple-500 border-neutral-300 rounded bg-white" %>
|
||||
<label for="user_remember_me" class="ml-2 block text-sm text-neutral-700"> Se souvenir de moi </label>
|
||||
</div>
|
||||
|
||||
<%= f.check_box :remember_me, class: "h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded bg-white" %>
|
||||
<%= f.label :remember_me, "Se souvenir de moi", class: "ml-2 block text-sm text-gray-700" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit "Se connecter", class: "group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-neutral-50 focus:ring-purple-500" %>
|
||||
<div class="pt-4">
|
||||
<%= f.button type: "submit", class: "group relative w-full flex justify-center items-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white bg-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" do %>
|
||||
<i data-lucide="log-in" class="w-4 h-4 mr-2"></i>
|
||||
Se connecter
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-neutral-300"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 bg-neutral-50 text-neutral-600">Continuer avec</span>
|
||||
<!-- Additional Links -->
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<div class="text-center space-y-3">
|
||||
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
|
||||
<%= link_to "Mot de passe oublié ?", new_password_path(resource_name), class: "text-sm text-purple-600 hover:text-purple-500 transition-colors" %>
|
||||
<% end %>
|
||||
<p class="text-sm text-gray-600">
|
||||
Vous n'avez pas encore de compte?
|
||||
<a href="<%= new_user_registration_path %>" class="font-semibold text-purple-600 hover:text-purple-500 transition-colors">S'inscrire</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +1,20 @@
|
||||
<% if resource.errors.any? %>
|
||||
<div class="bg-red-50 border border-red-200 text-red-800 rounded-xl p-4 mb-6" data-controller="flash-message">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<i data-lucide="x-circle" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<h3 class="text-sm font-medium mb-2">Veuillez corriger les erreurs suivantes :</h3>
|
||||
<ul class="list-disc list-inside space-y-1 text-sm">
|
||||
<% resource.errors.full_messages.each do |message| %>
|
||||
<% flash.now[:error] = message %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<button data-action="click->flash-message#close" class="bg-transparent border-none cursor-pointer p-1 text-inherit opacity-70 transition-opacity duration-200">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -1,39 +1,58 @@
|
||||
<div class="mt-4 space-y-4">
|
||||
<%- if controller_name != "sessions" %>
|
||||
<div class="w-full flex justify-center py-2 px-4 border border-neutral-300 rounded-md shadow-sm text-sm font-medium text-purple-600 bg-white hover:bg-neutral-50 hover:border-purple-500 transition-all duration-200">
|
||||
<%= link_to "Se connecter", new_session_path(resource_name), class: "block" %>
|
||||
<div class="mt-6">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-gray-200"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 bg-white text-gray-500">Ou continuer avec</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid grid-cols-2 gap-3">
|
||||
<%- if controller_name != "sessions" %>
|
||||
<%= link_to new_session_path(resource_name), class: "w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-xl shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all duration-200" do %>
|
||||
<i data-lucide="log-in" class="w-4 h-4 mr-2"></i>
|
||||
Se connecter
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.registerable? && controller_name != "registrations" %>
|
||||
<div class="w-full flex justify-center mt-4 py-2 px-4 border border-neutral-300 rounded-md shadow-sm text-sm font-medium text-purple-600 bg-white hover:bg-neutral-50 hover:border-purple-500 transition-all duration-200">
|
||||
<%= link_to "Créer un compte", new_registration_path(resource_name), class: "block" %>
|
||||
</div>
|
||||
<%= link_to new_registration_path(resource_name), class: "w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-xl shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all duration-200" do %>
|
||||
<i data-lucide="user-plus" class="w-4 h-4 mr-2"></i>
|
||||
S'inscrire
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.recoverable? && controller_name != "passwords" && controller_name != "registrations" %>
|
||||
<div class="w-full flex justify-center mt-4 py-2 px-4 border border-neutral-300 rounded-md shadow-sm text-sm font-medium text-purple-600 bg-white hover:bg-neutral-50 hover:border-purple-500 transition-all duration-200">
|
||||
<%= link_to "Mot de passe oublié ?", new_password_path(resource_name), class: "block" %>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 gap-3">
|
||||
<%- if devise_mapping.recoverable? && controller_name != "passwords" && controller_name != "registrations" %>
|
||||
<%= link_to new_password_path(resource_name), class: "w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-xl shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all duration-200" do %>
|
||||
<i data-lucide="help-circle" class="w-4 h-4 mr-2"></i>
|
||||
Mot de passe oublié ?
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.confirmable? && controller_name != "confirmations" %>
|
||||
<div class="w-full flex justify-center mt-4 py-2 px-4 border border-neutral-300 rounded-md shadow-sm text-sm font-medium text-purple-600 bg-white hover:bg-neutral-50 hover:border-purple-500 transition-all duration-200">
|
||||
<%= link_to "Renvoyer le lien de confirmation", new_confirmation_path(resource_name), class: "block" %>
|
||||
</div>
|
||||
<%= link_to new_confirmation_path(resource_name), class: "w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-xl shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all duration-200" do %>
|
||||
<i data-lucide="mail" class="w-4 h-4 mr-2"></i>
|
||||
Renvoyer le lien de confirmation
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != "unlocks" %>
|
||||
<div class="w-full flex justify-center mt-4 py-2 px-4 border border-neutral-300 rounded-md shadow-sm text-sm font-medium text-purple-600 bg-white hover:bg-neutral-50 hover:border-purple-500 transition-all duration-200">
|
||||
<%= link_to "Renvoyer le lien de déblocage", new_unlock_path(resource_name), class: "block" %>
|
||||
</div>
|
||||
<%= link_to new_unlock_path(resource_name), class: "w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-xl shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all duration-200" do %>
|
||||
<i data-lucide="unlock" class="w-4 h-4 mr-2"></i>
|
||||
Renvoyer le lien de déblocage
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.omniauthable? %>
|
||||
<%- resource_class.omniauth_providers.each do |provider| %>
|
||||
<div class="w-full flex justify-center mt-4 py-2 px-4 border border-neutral-300 rounded-md shadow-sm text-sm font-medium text-purple-600 bg-white hover:bg-neutral-50 hover:border-purple-500 transition-all duration-200">
|
||||
<%= button_to "Se connecter avec #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), data: { turbo: false }, class: "block" %>
|
||||
<%= button_to omniauth_authorize_path(resource_name, provider), data: { turbo: false }, class: "w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-xl shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all duration-200" do %>
|
||||
<i data-lucide="external-link" class="w-4 h-4 mr-2"></i>
|
||||
Se connecter avec <%= OmniAuth::Utils.camelize(provider) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -1,15 +1,52 @@
|
||||
<h2>Resend unlock instructions</h2>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :email %><br />
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
<!-- Header -->
|
||||
<div class="text-center">
|
||||
<%= link_to "/" do %>
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-purple-600 to-blue-600 rounded-2xl mb-6">
|
||||
<i data-lucide="calendar" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<% end %>
|
||||
<h2 class="text-3xl font-bold text-gray-900">Renvoyer les instructions de déverrouillage</h2>
|
||||
<p class="mt-2 text-gray-600">
|
||||
Entrez votre adresse email et nous vous enverrons les instructions de déverrouillage
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit "Resend unlock instructions" %>
|
||||
<!-- Form -->
|
||||
<div class="bg-white py-8 px-6 shadow-xl rounded-2xl">
|
||||
<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: "space-y-6" }) do |f| %>
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<%= f.label :email, "Adresse email", class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i data-lucide="mail" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email",
|
||||
placeholder: "votre@email.com",
|
||||
class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
<div class="pt-4">
|
||||
<%= f.button type: "submit", class: "group relative w-full flex justify-center items-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white bg-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" do %>
|
||||
<i data-lucide="send" class="w-4 h-4 mr-2"></i>
|
||||
Renvoyer les instructions de déverrouillage
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Additional Links -->
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
Vous vous souvenez de votre mot de passe ?
|
||||
<a href="<%= new_user_session_path %>" class="font-semibold text-purple-600 hover:text-purple-500 transition-colors">Se connecter</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,185 +1,116 @@
|
||||
<div class="container min-h-screen mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="flex justify-between items-center my-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900">Événements à venir</h1>
|
||||
<div class="text-sm text-gray-500">
|
||||
<%= @events.total_count %>
|
||||
événements trouvés
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="mb-6" aria-label="Breadcrumb">
|
||||
<ol class="flex items-center space-x-2 text-sm">
|
||||
<%= link_to root_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<svg
|
||||
class="w-4 h-4 inline-block mr-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
Accueil
|
||||
<% end %>
|
||||
<svg
|
||||
class="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
<%= link_to events_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
Événements
|
||||
<% end %>
|
||||
</ol>
|
||||
</nav>
|
||||
<%= render 'components/breadcrumb', crumbs: [
|
||||
{ name: 'Accueil', path: root_path },
|
||||
{ name: 'Événements', path: events_path }
|
||||
] %>
|
||||
|
||||
<!-- Page Header -->
|
||||
<header class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8 gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl lg:text-4xl font-bold text-gray-900">Événements à venir</h1>
|
||||
<p class="text-gray-600 mt-2">Découvrez les meilleurs afterworks et événements de Paris</p>
|
||||
</div>
|
||||
<div class="bg-purple-100 text-purple-800 px-4 py-2 rounded-full text-sm font-medium">
|
||||
<%= @events.total_count %> événements trouvés
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Events Grid -->
|
||||
<% if @events.any? %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<% @events.each do |event| %>
|
||||
<div
|
||||
class="
|
||||
bg-white rounded-xl shadow-md overflow-hidden hover:shadow-xl transition-all
|
||||
duration-300 transform hover:-translate-y-1
|
||||
"
|
||||
>
|
||||
<article class="group bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 overflow-hidden transform hover:-translate-y-1">
|
||||
<%= link_to event_path(event.slug, event), class: "block" do %>
|
||||
<% if event.image.present? %>
|
||||
<div class="h-48 overflow-hidden">
|
||||
<%= link_to event_path(event.slug, event) do %>
|
||||
<div class="relative overflow-hidden aspect-[4/3]">
|
||||
<img
|
||||
src="<%= event.image %>"
|
||||
alt="<%= event.name %>"
|
||||
class="featured-event-image"
|
||||
data-featured-event-target="animated"
|
||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
>
|
||||
<!-- Event featured badge -->
|
||||
<% if event.featured? %>
|
||||
<div class="absolute top-4 left-4">
|
||||
<span class="bg-yellow-400 text-gray-900 px-3 py-1 rounded-full text-sm font-medium shadow-lg">
|
||||
★ En vedette
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
<!-- Date badge -->
|
||||
<div class="absolute bottom-4 right-4">
|
||||
<span class="bg-white/90 backdrop-blur-sm text-gray-900 px-3 py-1 rounded-full text-sm font-bold shadow-lg">
|
||||
<%= event.start_time.strftime("%d/%m") %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div
|
||||
class="
|
||||
h-48 bg-gradient-to-r from-purple-500 to-indigo-600 flex items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<svg
|
||||
class="w-16 h-16 text-white opacity-80"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
</svg>
|
||||
<div class="relative overflow-hidden aspect-[4/3] bg-gradient-to-br from-purple-600 to-blue-600 flex items-center justify-center">
|
||||
<i data-lucide="calendar" class="w-16 h-16 text-white"></i>
|
||||
<!-- Date badge -->
|
||||
<div class="absolute bottom-4 right-4">
|
||||
<span class="bg-white/90 backdrop-blur-sm text-gray-900 px-3 py-1 rounded-full text-sm font-bold shadow-lg">
|
||||
<%= event.start_time.strftime("%d/%m") %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-gray-900 line-clamp-1"><%= event.name %></h2>
|
||||
<p class="text-xs text-gray-500 flex items-center mt-1">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
||||
/>
|
||||
</svg>
|
||||
<%= event.venue_name.truncate(20) %>
|
||||
<div class="mb-4">
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-2 group-hover:text-purple-600 transition-colors line-clamp-2"><%= event.name %></h2>
|
||||
<p class="text-sm text-gray-500 flex items-center">
|
||||
<i data-lucide="map-pin" class="w-4 h-4 mr-2"></i>
|
||||
<%= event.venue_name.truncate(25) %>
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 flex items-center mt-1">
|
||||
<i data-lucide="clock" class="w-4 h-4 mr-2"></i>
|
||||
<%= l(event.start_time, format: '%A %d %B • %H:%M') %>
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
class="
|
||||
inline-flex items-center my-2 px-2.5 py-2 rounded-full text-xs font-medium
|
||||
bg-purple-100 text-purple-800
|
||||
"
|
||||
>
|
||||
<%= event.start_time.strftime("%d/%m") %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-gray-600 text-sm line-clamp-2"><%= event.description.truncate(100) %></p>
|
||||
</div>
|
||||
<p class="text-gray-600 text-sm mb-4 line-clamp-2">
|
||||
<%= event.description.truncate(100) %>
|
||||
</p>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex justify-between items-center pt-4 border-t border-gray-100">
|
||||
<div>
|
||||
<% if event.ticket_types.any? %>
|
||||
<p class="text-sm font-medium text-gray-900">
|
||||
À partir de
|
||||
<%= format_price(event.ticket_types.minimum(:price_cents)) %>€
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
À 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 event_path(event.slug, event), class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all duration-200" do %>
|
||||
Détails
|
||||
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
<div class="inline-flex items-center text-purple-600 font-medium text-sm group-hover:text-purple-700">
|
||||
Voir détails
|
||||
<i data-lucide="arrow-right" class="w-4 h-4 ml-1 group-hover:translate-x-1 transition-transform"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex justify-center">
|
||||
<!-- Pagination -->
|
||||
<div class="flex justify-center mt-12">
|
||||
<%= paginate @events, theme: "tailwind" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<!-- Empty State -->
|
||||
<div class="text-center py-16">
|
||||
<div class="mx-auto max-w-md">
|
||||
<div
|
||||
class="
|
||||
w-24 h-24 mx-auto bg-gradient-to-r from-purple-100 to-indigo-100 rounded-full
|
||||
flex items-center justify-center mb-6
|
||||
"
|
||||
>
|
||||
<svg
|
||||
class="w-12 h-12 text-purple-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Aucun événement disponible</h3>
|
||||
<p class="text-gray-500 mb-6">Il n'y a aucun événement à venir pour le moment.</p>
|
||||
<%= link_to "Retour à l'accueil",
|
||||
root_path,
|
||||
class:
|
||||
"inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500" %>
|
||||
<div class="w-24 h-24 bg-gradient-to-br from-purple-100 to-blue-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<i data-lucide="calendar-x" class="w-12 h-12 text-purple-600"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">Aucun événement disponible</h3>
|
||||
<p class="text-gray-600 mb-8 max-w-md mx-auto">Il n'y a aucun événement à venir pour le moment. Revenez bientôt pour découvrir de nouvelles sorties!</p>
|
||||
<%= link_to "Retour à l'accueil", root_path, class: "inline-flex items-center bg-purple-600 text-white px-6 py-3 rounded-full font-semibold hover:bg-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" do %>
|
||||
<i data-lucide="home" class="w-4 h-4 mr-2"></i>
|
||||
Retour à l'accueil
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -84,8 +84,8 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<%= link_to download_ticket_path(ticket, format: :pdf),
|
||||
class: "inline-flex items-center px-4 py-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white rounded-lg hover:from-purple-700 hover:to-indigo-700 transition-all duration-200 text-sm font-medium shadow-sm" do %>
|
||||
<%= link_to ticket_download_path(ticket.qr_code, format: :pdf),
|
||||
class: "inline-flex items-center px-4 py-2 btn btn-primary rounded-lg transition-all duration-200 text-sm font-medium shadow-sm" do %>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
@@ -129,7 +129,7 @@
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<%= link_to dashboard_path,
|
||||
class: "inline-flex items-center justify-center px-6 py-3 bg-gradient-to-r from-purple-600 to-indigo-600 text-white rounded-xl hover:from-purple-700 hover:to-indigo-700 transition-all duration-200 font-medium shadow-sm" do %>
|
||||
class: "inline-flex items-center justify-center px-6 py-3 btn btn-primary rounded-xl transition-all duration-200 font-medium shadow-sm" do %>
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
||||
</svg>
|
||||
|
||||
@@ -1,60 +1,19 @@
|
||||
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="mb-6" aria-label="Breadcrumb">
|
||||
<ol class="flex items-center space-x-2 text-sm">
|
||||
<%= link_to root_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
Accueil
|
||||
<% end %>
|
||||
<svg
|
||||
class="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
<%= link_to events_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
Événements
|
||||
<% end %>
|
||||
<svg
|
||||
class="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
<li class="font-medium text-gray-900 truncate max-w-xs" aria-current="page">
|
||||
<%= @event.name %>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<%= render 'components/breadcrumb', crumbs: [
|
||||
{ name: 'Accueil', path: root_path },
|
||||
{ name: 'Événements', path: events_path },
|
||||
{ name: @event.name, path: nil }
|
||||
] %>
|
||||
|
||||
<!-- Event main wrapper -->
|
||||
<div class="bg-white rounded-xl shadow-xl overflow-hidden">
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<!-- Event Header with Image -->
|
||||
<% if @event.image.present? %>
|
||||
<div class="relative h-96">
|
||||
<%= image_tag @event.image, class: "w-full h-full object-cover" %>
|
||||
<div
|
||||
class="
|
||||
absolute inset-0 bg-gradient-to-t from-black via-black/70 to-transparent
|
||||
"
|
||||
></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/70 to-transparent"></div>
|
||||
<div class="absolute bottom-0 left-0 right-0 p-6 md:p-8">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-white mb-2 text-center md:text-left"><%= @event.name %></h1>
|
||||
@@ -66,46 +25,17 @@
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-white mb-4"><%= @event.name %></h1>
|
||||
<div class="flex flex-wrap items-center gap-4 text-white/90">
|
||||
<div class="flex items-center">
|
||||
<svg
|
||||
class="w-5 h-5 mr-2 text-purple-200"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
||||
></path>
|
||||
<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>
|
||||
<i data-lucide="map-pin" class="w-5 h-5 mr-2 text-purple-200"></i>
|
||||
<span><%= @event.venue_name %></span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<svg
|
||||
class="w-5 h-5 mr-2 text-purple-200"
|
||||
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>
|
||||
<i data-lucide="clock" class="w-5 h-5 mr-2 text-purple-200"></i>
|
||||
<span><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Event Content -->
|
||||
<div class="p-6 md:p-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
@@ -121,69 +51,73 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div class="bg-gray-50 rounded-xl p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<svg
|
||||
class="w-5 h-5 mr-2 text-purple-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
||||
></path>
|
||||
<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>
|
||||
<i data-lucide="map-pin" class="w-5 h-5 mr-2 text-purple-600"></i>
|
||||
Lieu
|
||||
</h3>
|
||||
<p class="text-gray-700 font-medium"><%= @event.venue_name %></p>
|
||||
<p class="text-gray-600 mt-1"><%= @event.venue_address %></p>
|
||||
<p class="text-gray-600 mt-2 mb-4"><%= @event.venue_address %></p>
|
||||
|
||||
<% if @event.latitude.present? && @event.longitude.present? %>
|
||||
<div class="border-t border-gray-200 pt-4">
|
||||
<% if @event.geocoding_status_message %>
|
||||
<div class="mb-3 p-2 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="alert-triangle" class="w-4 h-4 text-yellow-600 mr-2"></i>
|
||||
<p class="text-xs text-yellow-800"><%= @event.geocoding_status_message %></p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<p class="text-sm font-medium text-gray-700 mb-2">Ouvrir dans :</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<%
|
||||
encoded_address = URI.encode_www_form_component(@event.venue_address)
|
||||
lat = @event.latitude
|
||||
lng = @event.longitude
|
||||
|
||||
map_providers = {
|
||||
"OpenStreetMap" => "https://www.openstreetmap.org/?mlat=#{lat}&mlon=#{lng}#map=16/#{lat}/#{lng}",
|
||||
"Google Maps" => "https://www.google.com/maps/search/#{encoded_address}/@#{lat},#{lng},16z",
|
||||
"Apple Plans" => "https://maps.apple.com/?address=#{encoded_address}&ll=#{lat},#{lng}"
|
||||
}
|
||||
|
||||
icons = {
|
||||
"OpenStreetMap" => "🗺️",
|
||||
"Google Maps" => "🔍",
|
||||
"Apple Plans" => "🍎"
|
||||
}
|
||||
%>
|
||||
|
||||
<% map_providers.each do |name, url| %>
|
||||
<%= link_to url, target: "_blank", rel: "noopener",
|
||||
class: "inline-flex items-center px-3 py-2 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors" do %>
|
||||
<span class="mr-1"><%= icons[name] %></span>
|
||||
<%= name %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 rounded-xl p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<svg
|
||||
class="w-5 h-5 mr-2 text-purple-600"
|
||||
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>
|
||||
<i data-lucide="clock" class="w-5 h-5 mr-2 text-purple-600"></i>
|
||||
Date & Heure
|
||||
</h3>
|
||||
<p class="text-gray-700 font-medium"><%= @event.start_time.strftime("%A %d %B %Y") %></p>
|
||||
<p class="text-gray-600 mt-1">À
|
||||
<%= @event.start_time.strftime("%H:%M") %></p>
|
||||
<p class="text-gray-600 mt-1">À <%= @event.start_time.strftime("%H:%M") %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-8 bg-gray-50 rounded-xl p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Organisateur</h3>
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="
|
||||
w-12 h-12 rounded-full bg-gradient-to-r from-purple-500 to-indigo-600 flex
|
||||
items-center justify-center text-white font-bold
|
||||
"
|
||||
>
|
||||
<div class="w-12 h-12 rounded-full bg-purple-500 flex items-center justify-center text-white font-bold">
|
||||
<%= @event.user.email.first.upcase %>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<% if @event.user.first_name.present? && @event.user.last_name.present? %>
|
||||
<p class="font-medium text-gray-900"><%= @event.user.first_name %>
|
||||
<%= @event.user.last_name %></p>
|
||||
<p class="font-medium text-gray-900"><%= @event.user.first_name %> <%= @event.user.last_name %></p>
|
||||
<% else %>
|
||||
<p class="font-medium text-gray-900"><%= @event.user.email.split("@").first %></p>
|
||||
<% end %>
|
||||
@@ -193,7 +127,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- Left Column: Event Details -->
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Ticket Selection -->
|
||||
<div class="lg:col-span-1">
|
||||
@@ -204,26 +138,19 @@
|
||||
ticket_selection_event_id_value: @event.id
|
||||
} do |form| %>
|
||||
|
||||
<div class="">
|
||||
<div
|
||||
class="
|
||||
bg-gradient-to-br from-purple-50 to-indigo-50 rounded-2xl border
|
||||
border-purple-100 p-6 shadow-sm
|
||||
"
|
||||
>
|
||||
<div class="bg-gradient-to-br from-purple-50 to-indigo-50 rounded-2xl border border-purple-100 p-6 shadow-sm">
|
||||
<div class="flex justify-center sm:justify-start mb-6">
|
||||
<h2 class="text-lg font-bold text-gray-900">Billets disponibles</h2>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<div>
|
||||
<% if @event.ticket_types.any? %>
|
||||
<div class="space-y-4 mb-6">
|
||||
<% @event.ticket_types.each do |ticket_type| %>
|
||||
<% sold_out = ticket_type.quantity <= ticket_type.tickets.count %>
|
||||
<% remaining = ticket_type.quantity - ticket_type.tickets.count %>
|
||||
|
||||
<%= render "components/ticket_card",
|
||||
{
|
||||
<%= render "components/ticket_card", {
|
||||
id: ticket_type.id,
|
||||
name: ticket_type.name,
|
||||
description: ticket_type.description,
|
||||
@@ -236,24 +163,12 @@
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-center py-8">
|
||||
<svg
|
||||
class="w-12 h-12 mx-auto text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"
|
||||
/>
|
||||
</svg>
|
||||
<i data-lucide="ticket" class="w-12 h-12 mx-auto text-gray-400"></i>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900">Aucun billet disponible</h3>
|
||||
<p class="mt-2 text-gray-500">Les billets pour cet événement ne sont pas encore
|
||||
disponibles ou sont épuisés.</p>
|
||||
<p class="mt-2 text-gray-500">Les billets pour cet événement ne sont pas encore disponibles ou sont épuisés.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Cart Summary -->
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
@@ -264,17 +179,16 @@
|
||||
<span class="text-gray-600">Montant total :</span>
|
||||
<span class="text-xl font-bold text-purple-700" data-ticket-selection-target="totalAmount">€0.00</span>
|
||||
</div>
|
||||
<%= form.submit "Procéder au paiement",
|
||||
<%= form.button "Procéder au paiement", type: "submit",
|
||||
data: { ticket_selection_target: "checkoutButton" },
|
||||
class: "w-full bg-gradient-to-r from-purple-600 to-indigo-600 text-white font-medium py-3 px-4 rounded-xl shadow-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 flex items-center justify-center opacity-50 cursor-not-allowed",
|
||||
disabled: true %>
|
||||
class: "w-full btn btn-primary py-3 px-4 rounded-xl shadow-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 flex items-center justify-center opacity-50 cursor-not-allowed" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div><!-- Right Column: Ticket Selection -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<title><%= content_for(:title) || "Aperonight" %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
@@ -10,11 +10,11 @@
|
||||
<%= yield :head %>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Outfit:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<!--<link rel="preconnect" href="https://fonts.googleapis.com">-->
|
||||
<!--<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>-->
|
||||
<!--<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=DM+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">-->
|
||||
|
||||
<!-- Lucide Icons loaded via npm package -->
|
||||
<!-- Lucide Icons -->
|
||||
|
||||
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
|
||||
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
||||
@@ -28,21 +28,19 @@
|
||||
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
||||
|
||||
</head>
|
||||
<body data-user-authenticated="<%= user_signed_in? %>" data-event-slug="<%= @event&.slug %>">
|
||||
<body data-user-authenticated="<%= user_signed_in? %>" data-event-slug="<%= @event&.slug %>" class="font-sans bg-white text-gray-900">
|
||||
<div class="app-wrapper">
|
||||
<%= render "components/header" %>
|
||||
|
||||
<!-- Flash messages positioned between header and content -->
|
||||
<%= render "shared/flash_messages" %>
|
||||
|
||||
<main class="">
|
||||
<div class="yield">
|
||||
<main class="flex-1">
|
||||
<%= yield %>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="bg-neutral-800 text-neutral-300 py-8 pb-4">
|
||||
<div class="container">
|
||||
<footer class="bg-gray-900 text-white py-16">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<%= render "components/footer" %>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><%= yield :title %></title>
|
||||
<%= stylesheet_link_tag "pdf" %>
|
||||
</head>
|
||||
<body>
|
||||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
||||
101
app/views/onboarding/index.html.erb
Normal file
101
app/views/onboarding/index.html.erb
Normal file
@@ -0,0 +1,101 @@
|
||||
<div class="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-50 py-8">
|
||||
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="mx-auto w-20 h-20 bg-purple-100 rounded-full flex items-center justify-center mb-6">
|
||||
<i data-lucide="user" class="w-10 h-10 text-purple-600"></i>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-3">Bienvenue sur <%= Rails.application.config.app_name %> !</h1>
|
||||
<p class="text-lg text-gray-600 max-w-lg mx-auto">
|
||||
Configurons rapidement votre profil pour personnaliser votre expérience.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Onboarding Form -->
|
||||
<div class="bg-white rounded-2xl shadow-xl p-8">
|
||||
<%= form_with model: current_user, url: complete_onboarding_path, local: true, method: :post, class: "space-y-6" do |form| %>
|
||||
|
||||
<!-- Progress indicator -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between text-xs text-gray-500 mb-2">
|
||||
<span>Étape 1 sur 1</span>
|
||||
<span>Configuration du profil</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-purple-600 h-2 rounded-full w-full transition-all duration-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Fields -->
|
||||
<div class="space-y-6">
|
||||
<!-- Personal Information Section -->
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<i data-lucide="user" class="w-5 h-5 mr-2 text-purple-600"></i>
|
||||
Informations personnelles
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- First Name -->
|
||||
<div>
|
||||
<%= form.label :first_name, "Prénom", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.text_field :first_name,
|
||||
value: current_user.first_name,
|
||||
class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors",
|
||||
placeholder: "Votre prénom",
|
||||
required: true %>
|
||||
</div>
|
||||
|
||||
<!-- Last Name -->
|
||||
<div>
|
||||
<%= form.label :last_name, "Nom", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.text_field :last_name,
|
||||
value: current_user.last_name,
|
||||
class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors",
|
||||
placeholder: "Votre nom de famille",
|
||||
required: true %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="pt-6 border-t border-gray-200">
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-gray-500">
|
||||
Vous pourrez modifier ces informations plus tard.
|
||||
</p>
|
||||
<%= form.button type: "submit", class: "w-full px-8 py-3 bg-purple-600 text-white font-semibold rounded-lg hover:bg-purple-700 focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transition-colors cursor-pointer flex items-center justify-center" do %>
|
||||
<i data-lucide="check" class="w-4 h-4 mr-2"></i>
|
||||
Compléter mon profil
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Benefits Preview -->
|
||||
<div class="mt-8 bg-white rounded-xl shadow-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4 text-center">
|
||||
Après la configuration, vous pourrez :
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="flex items-center p-3 bg-green-50 rounded-lg">
|
||||
<i data-lucide="calendar" class="w-6 h-6 text-green-600 mr-3"></i>
|
||||
<span class="text-sm font-medium text-green-800">Réserver des billets</span>
|
||||
</div>
|
||||
<div class="flex items-center p-3 bg-blue-50 rounded-lg">
|
||||
<i data-lucide="clock" class="w-6 h-6 text-blue-600 mr-3"></i>
|
||||
<span class="text-sm font-medium text-blue-800">Gérer vos commandes</span>
|
||||
</div>
|
||||
<div class="flex items-center p-3 bg-purple-50 rounded-lg">
|
||||
<i data-lucide="settings" class="w-6 h-6 text-purple-600 mr-3"></i>
|
||||
<span class="text-sm font-medium text-purple-800">Créer des événements</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,31 +1,29 @@
|
||||
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 py-8">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="mb-8" aria-label="Breadcrumb">
|
||||
<ol class="flex items-center space-x-2 text-sm">
|
||||
<%= link_to root_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
</svg>
|
||||
<nav class="inline-flex items-center gap-2 bg-white px-4 py-3 rounded-xl shadow-sm border border-gray-100 mb-8" aria-label="Breadcrumb">
|
||||
<div class="inline-flex items-center text-sm font-medium">
|
||||
<%= link_to root_path, class: "text-gray-700 hover:text-purple-600 transition-colors" do %>
|
||||
<i data-lucide="home" class="w-4 h-4 mr-2"></i>
|
||||
Accueil
|
||||
<% end %>
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<%= link_to events_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
</div>
|
||||
<i data-lucide="chevron-right" class="w-4 h-4 text-gray-400"></i>
|
||||
<div class="inline-flex items-center text-sm font-medium">
|
||||
<%= link_to events_path, class: "text-gray-700 hover:text-purple-600 transition-colors" do %>
|
||||
Événements
|
||||
<% end %>
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<%= link_to event_path(@order.event.slug, @order.event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
</div>
|
||||
<i data-lucide="chevron-right" class="w-4 h-4 text-gray-400"></i>
|
||||
<div class="inline-flex items-center text-sm font-medium">
|
||||
<%= link_to event_path(@order.event.slug, @order.event), class: "text-gray-700 hover:text-purple-600 transition-colors" do %>
|
||||
<%= @order.event.name %>
|
||||
<% end %>
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<li class="font-medium text-gray-900" aria-current="page">Commande #<%= @order.id %></li>
|
||||
</ol>
|
||||
</div>
|
||||
<i data-lucide="chevron-right" class="w-4 h-4 text-gray-400"></i>
|
||||
<div class="text-sm font-medium text-purple-600">
|
||||
Commande #<%= @order.id %>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
@@ -35,9 +33,7 @@
|
||||
<% if @expiring_soon %>
|
||||
<div class="mb-6 bg-orange-50 border border-orange-200 rounded-lg p-4">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-orange-600 mr-2 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<i data-lucide="alert-triangle" class="w-5 h-5 text-orange-600 mr-2 mt-0.5 flex-shrink-0"></i>
|
||||
<div>
|
||||
<h3 class="font-medium text-orange-800 mb-1">Attention - Commande bientôt expirée</h3>
|
||||
<p class="text-orange-700 text-sm">Votre commande va expirer dans quelques minutes. Veuillez procéder rapidement au paiement pour éviter son expiration automatique.</p>
|
||||
@@ -50,9 +46,7 @@
|
||||
<% if @order.payment_attempts > 0 %>
|
||||
<div class="mb-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-blue-600 mr-2 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<i data-lucide="info" class="w-5 h-5 text-blue-600 mr-2 mt-0.5 flex-shrink-0"></i>
|
||||
<div>
|
||||
<h3 class="font-medium text-blue-800 mb-1">Nouvelle tentative de paiement</h3>
|
||||
<p class="text-blue-700 text-sm">
|
||||
@@ -70,17 +64,13 @@
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-2">Commande pour <%= @order.event.name %></h1>
|
||||
<div class="flex items-center text-sm text-gray-600 space-x-4">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" 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"/>
|
||||
</svg>
|
||||
<i data-lucide="clock" class="w-4 h-4 mr-1"></i>
|
||||
<% if @order.expires_at %>
|
||||
Expire dans <%= time_ago_in_words(@order.expires_at, include_seconds: true) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
<i data-lucide="file-text" class="w-4 h-4 mr-1"></i>
|
||||
Commande #<%= @order.id %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,9 +85,7 @@
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="text-sm font-medium text-gray-900 truncate"><%= ticket.ticket_type.name %></h4>
|
||||
<div class="flex items-center text-xs text-gray-500 mt-1">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
<i data-lucide="user" class="w-3 h-3 mr-1"></i>
|
||||
<%= ticket.first_name %> <%= ticket.last_name %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,10 +101,20 @@
|
||||
|
||||
<!-- Order Total -->
|
||||
<div class="border-t border-gray-200 pt-6">
|
||||
<div class="flex items-center justify-between text-lg">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600">Sous-total</span>
|
||||
<span class="text-gray-900"><%= @order.total_amount_euros - 1.0 %>€</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600">Frais de service</span>
|
||||
<span class="text-gray-900">1.00€</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-lg pt-2 border-t border-gray-200">
|
||||
<span class="font-medium text-gray-900">Total</span>
|
||||
<span class="font-bold text-2xl text-purple-600"><%= @order.total_amount_euros %>€</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">TVA incluse</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,9 +131,7 @@
|
||||
<div class="space-y-6">
|
||||
<div class="bg-gradient-to-r from-purple-50 to-pink-50 rounded-lg p-4 border border-purple-200">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-purple-600 mr-2 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
|
||||
</svg>
|
||||
<i data-lucide="shield" class="w-5 h-5 text-purple-600 mr-2 mt-0.5 flex-shrink-0"></i>
|
||||
<div>
|
||||
<h3 class="font-medium text-purple-800 mb-1">Paiement 100% sécurisé</h3>
|
||||
<p class="text-purple-700 text-sm">Vos données bancaires sont protégées par le cryptage SSL et traitées par Stripe, leader mondial du paiement en ligne.</p>
|
||||
@@ -145,33 +141,25 @@
|
||||
|
||||
<button
|
||||
id="checkout-button"
|
||||
class="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white font-bold py-4 px-6 rounded-xl transition-all duration-200 transform hover:scale-105 active:scale-95 shadow-lg hover:shadow-xl"
|
||||
class="w-full btn btn-primary py-4 px-6 rounded-xl transition-all duration-200 transform hover:scale-105 active:scale-95 shadow-lg hover:shadow-xl"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>
|
||||
</svg>
|
||||
<i data-lucide="credit-card" class="w-5 h-5 mr-2"></i>
|
||||
Payer <%= @order.total_amount_euros %>€
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center justify-center space-x-4 text-xs text-gray-500">
|
||||
<span class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>
|
||||
</svg>
|
||||
<i data-lucide="credit-card" class="w-4 h-4 mr-1"></i>
|
||||
Visa
|
||||
</span>
|
||||
<span class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>
|
||||
</svg>
|
||||
<i data-lucide="credit-card" class="w-4 h-4 mr-1"></i>
|
||||
Mastercard
|
||||
</span>
|
||||
<span class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
|
||||
</svg>
|
||||
<i data-lucide="shield" class="w-4 h-4 mr-1"></i>
|
||||
Sécurisé par Stripe
|
||||
</span>
|
||||
</div>
|
||||
@@ -212,10 +200,9 @@
|
||||
try {
|
||||
// Increment payment attempt counter
|
||||
console.log('Incrementing payment attempt for order:', '<%= @order.id %>');
|
||||
const response = await fetch('<%= increment_payment_attempt_order_path(@order) %>', {
|
||||
method: 'POST',
|
||||
const response = await fetch('/api/v1/orders/<%= @order.id %>/increment_payment_attempt', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
@@ -254,9 +241,7 @@
|
||||
button.disabled = false;
|
||||
button.innerHTML = `
|
||||
<div class="flex items-center justify-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>
|
||||
</svg>
|
||||
<i data-lucide="credit-card" class="w-5 h-5 mr-2"></i>
|
||||
Payer <%= @order.total_amount_euros %>€
|
||||
</div>
|
||||
`;
|
||||
@@ -277,9 +262,7 @@
|
||||
<!-- No Stripe Configuration -->
|
||||
<div class="text-center py-8">
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-6">
|
||||
<svg class="w-12 h-12 text-yellow-600 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<i data-lucide="alert-triangle" class="w-12 h-12 text-yellow-600 mx-auto mb-4"></i>
|
||||
<h3 class="font-semibold text-yellow-800 mb-2">Paiement temporairement indisponible</h3>
|
||||
<p class="text-yellow-700 text-sm">Le système de paiement n'est pas encore configuré. Veuillez contacter l'organisateur pour plus d'informations.</p>
|
||||
</div>
|
||||
@@ -291,9 +274,7 @@
|
||||
<div class="space-y-3">
|
||||
<%= link_to event_path(@order.event.slug, @order.event), class: "block w-full text-center py-3 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors" do %>
|
||||
<div class="flex items-center justify-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
<i data-lucide="arrow-left" class="w-4 h-4 mr-2"></i>
|
||||
Retour à l'événement
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
131
app/views/orders/index.html.erb
Normal file
131
app/views/orders/index.html.erb
Normal file
@@ -0,0 +1,131 @@
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="flex my-6" aria-label="Breadcrumb">
|
||||
<ol class="inline-flex items-center space-x-1 md:space-x-2 rounded-lg bg-white px-4 py-2 shadow-sm">
|
||||
<li class="inline-flex items-center">
|
||||
<%= link_to "Accueil", root_path, class: "inline-flex items-center text-sm font-medium text-gray-700 hover:text-purple-600" %>
|
||||
</li>
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<%= link_to "Tableau de bord", dashboard_path, class: "ml-1 text-sm font-medium text-gray-700 hover:text-purple-600 md:ml-2" %>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span class="ml-1 text-sm font-medium text-purple-600 md:ml-2">Toutes mes commandes</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-slate-100">Toutes mes commandes</h1>
|
||||
<p class="text-slate-600 dark:text-slate-400 mt-2">Consultez l'historique de toutes vos commandes</p>
|
||||
</div>
|
||||
|
||||
<%= link_to dashboard_path, class: "inline-flex items-center px-4 py-2 bg-purple-100 hover:bg-purple-200 text-purple-700 font-medium rounded-lg transition-colors duration-200" do %>
|
||||
<i data-lucide="arrow-left" class="w-4 h-4 mr-2"></i>
|
||||
Retour au tableau de bord
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Orders List -->
|
||||
<% if @orders.any? %>
|
||||
<div class="space-y-6">
|
||||
<% @orders.each do |order| %>
|
||||
<div class="card hover-lift">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-3 mb-2">
|
||||
<h3 class="font-semibold text-slate-900 dark:text-slate-100"><%= order.event.name %></h3>
|
||||
<span class="text-xs px-2 py-1 rounded-full <%= order.status == 'paid' ? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100' : order.status == 'completed' ? 'bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100' : 'bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100' %>">
|
||||
<%= order.status.humanize %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4 text-sm text-slate-600 dark:text-slate-400 mb-3">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="calendar" class="w-4 h-4 mr-1"></i>
|
||||
<%= order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="map-pin" class="w-4 h-4 mr-1"></i>
|
||||
<%= order.event.venue_name %>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="shopping-bag" class="w-4 h-4 mr-1"></i>
|
||||
<%= pluralize(order.tickets.count, 'billet') %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">
|
||||
Commande #<%= order.id %> • <%= order.created_at.strftime("%d/%m/%Y") %> • <%= order.total_amount_euros %>€
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2 ml-4">
|
||||
<%= link_to order_path(order),
|
||||
class: "inline-flex items-center px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium rounded-lg transition-colors duration-200" do %>
|
||||
<i data-lucide="eye" class="w-4 h-4 mr-2"></i>
|
||||
Voir détails
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick tickets preview -->
|
||||
<div class="border-t border-slate-200 dark:border-slate-600 pt-3">
|
||||
<div class="grid gap-2">
|
||||
<% order.tickets.limit(3).each do |ticket| %>
|
||||
<div class="flex items-center justify-between text-sm bg-slate-50 dark:bg-slate-700 rounded p-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full"></span>
|
||||
<span class="font-medium"><%= ticket.ticket_type.name %></span>
|
||||
<span class="text-slate-500">- <%= ticket.first_name %> <%= ticket.last_name %></span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<%= link_to ticket_download_path(ticket.qr_code),
|
||||
class: "text-purple-600 hover:text-purple-800 dark:text-purple-400 dark:hover:text-purple-200" do %>
|
||||
<i data-lucide="download" class="w-3 h-3"></i>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if order.tickets.count > 3 %>
|
||||
<div class="text-xs text-slate-500 text-center">
|
||||
et <%= order.tickets.count - 3 %> autre<%= order.tickets.count - 3 > 1 ? 's' : '' %> billet<%= order.tickets.count - 3 > 1 ? 's' : '' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="mt-8">
|
||||
<%= paginate @orders %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-center py-12">
|
||||
<div class="w-16 h-16 bg-slate-100 dark:bg-slate-700 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i data-lucide="shopping-bag" class="w-8 h-8 text-slate-400"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-slate-900 dark:text-slate-100 mb-2">Aucune commande</h3>
|
||||
<p class="text-slate-600 dark:text-slate-400 mb-6">Vous n'avez encore passé aucune commande.</p>
|
||||
<%= link_to events_path, class: "inline-flex items-center px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors duration-200" do %>
|
||||
<i data-lucide="search" class="w-4 h-4 mr-2"></i>
|
||||
Découvrir les événements
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
154
app/views/orders/invoice.html.erb
Normal file
154
app/views/orders/invoice.html.erb
Normal file
@@ -0,0 +1,154 @@
|
||||
<div class="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-50 py-8">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Breadcrumb -->
|
||||
<%= render 'components/breadcrumb', crumbs: [
|
||||
{ name: 'Accueil', path: root_path },
|
||||
{ name: 'Tableau de bord', path: dashboard_path },
|
||||
{ name: "Commande ##{@order.id}", path: order_path(@order) },
|
||||
{ name: 'Facture', path: nil }
|
||||
] %>
|
||||
|
||||
<!-- Invoice Header -->
|
||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8 mb-8">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Facture</h1>
|
||||
<p class="text-gray-600">Commande #<%= @order.id %> • <%= @order.created_at.strftime("%d %B %Y") %></p>
|
||||
</div>
|
||||
<div class="mt-4 md:mt-0">
|
||||
<% if @stripe_invoice_pdf_url %>
|
||||
<%= link_to @stripe_invoice_pdf_url, target: "_blank", class: "inline-flex items-center px-4 py-2 bg-purple-600 text-white font-medium rounded-lg hover:bg-purple-700 transition-colors" do %>
|
||||
<i data-lucide="download" class="w-4 h-4 mr-2"></i>
|
||||
Télécharger la facture (PDF)
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invoice Details -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
|
||||
<!-- From -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-3">Émis par</h3>
|
||||
<div class="bg-purple-50 rounded-lg p-4 border border-purple-200">
|
||||
<h4 class="font-semibold text-purple-900"><%= ENV.fetch("INVOICE_COMPANY_NAME", "AperoNight") %></h4>
|
||||
<div class="mt-2 space-y-1 text-sm text-purple-700">
|
||||
<% if ENV["INVOICE_COMPANY_ADDRESS_LINE_1"].present? %>
|
||||
<p><%= ENV["INVOICE_COMPANY_ADDRESS_LINE_1"] %></p>
|
||||
<% end %>
|
||||
<% if ENV["INVOICE_COMPANY_ADDRESS_LINE_2"].present? %>
|
||||
<p><%= ENV["INVOICE_COMPANY_ADDRESS_LINE_2"] %></p>
|
||||
<% end %>
|
||||
<% if ENV["INVOICE_COMPANY_EMAIL"].present? %>
|
||||
<p><%= ENV["INVOICE_COMPANY_EMAIL"] %></p>
|
||||
<% end %>
|
||||
<% if ENV["INVOICE_COMPANY_PHONE"].present? %>
|
||||
<p><%= ENV["INVOICE_COMPANY_PHONE"] %></p>
|
||||
<% end %>
|
||||
<% if ENV["INVOICE_COMPANY_VAT_NUMBER"].present? %>
|
||||
<p>TVA: <%= ENV["INVOICE_COMPANY_VAT_NUMBER"] %></p>
|
||||
<% end %>
|
||||
<% if ENV["INVOICE_COMPANY_SIRET"].present? %>
|
||||
<p>SIRET: <%= ENV["INVOICE_COMPANY_SIRET"] %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- To -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-3">Facturé à</h3>
|
||||
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
||||
<h4 class="font-semibold text-gray-900">
|
||||
<%= @order.user.first_name %> <%= @order.user.last_name %>
|
||||
</h4>
|
||||
<div class="mt-2 space-y-1 text-sm text-gray-600">
|
||||
<p><%= @order.user.email %></p>
|
||||
<% if @order.user.company_name.present? %>
|
||||
<p><%= @order.user.company_name %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Information -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-3">Événement</h3>
|
||||
<div class="bg-indigo-50 rounded-lg p-4 border border-indigo-200">
|
||||
<h4 class="font-semibold text-indigo-900 text-lg"><%= @order.event.name %></h4>
|
||||
<div class="mt-2 space-y-1 text-sm text-indigo-700">
|
||||
<% if @order.event.start_time %>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="clock" class="w-4 h-4 mr-2"></i>
|
||||
<%= @order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @order.event.venue_name.present? %>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="map-pin" class="w-4 h-4 mr-2"></i>
|
||||
<%= @order.event.venue_name %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Items -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Détails de la facture</h3>
|
||||
<div class="overflow-hidden border border-gray-200 rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
|
||||
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Quantité</th>
|
||||
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Prix unitaire</th>
|
||||
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<% @tickets.group_by(&:ticket_type).each do |ticket_type, tickets| %>
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900"><%= ticket_type.name %></div>
|
||||
<div class="text-sm text-gray-500"><%= ticket_type.description %></div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right"><%= tickets.count %></td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right"><%= "%.2f" % (ticket_type.price_cents / 100.0) %>€</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 text-right"><%= "%.2f" % (tickets.count * ticket_type.price_cents / 100.0) %>€</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">Frais de service</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right">1</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right">1.00€</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 text-right">1.00€</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot class="bg-gray-50">
|
||||
<tr>
|
||||
<th colspan="3" scope="col" class="px-6 py-3 text-right text-sm font-medium text-gray-900 uppercase tracking-wider">Total</th>
|
||||
<th scope="col" class="px-6 py-3 text-right text-lg font-bold text-gray-900"><%= "%.2f" % @order.total_amount_euros %>€</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment Information -->
|
||||
<div class="border-t border-gray-200 pt-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-3">Paiement</h3>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<i data-lucide="check-circle" class="w-4 h-4 text-green-600"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h4 class="font-medium text-gray-900">Paiement effectué</h4>
|
||||
<p class="text-sm text-gray-600">Commande #<%= @order.id %> payée le <%= @order.updated_at.strftime("%d %B %Y") %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,34 +1,11 @@
|
||||
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="mb-8" aria-label="Breadcrumb">
|
||||
<ol class="flex items-center space-x-2 text-sm">
|
||||
<%= link_to root_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
</svg>
|
||||
Accueil
|
||||
<% end %>
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<%= link_to events_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
Événements
|
||||
<% end %>
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<%= link_to event_path(@event.slug, @event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<%= @event.name %>
|
||||
<% end %>
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<li class="font-medium text-gray-900" aria-current="page">
|
||||
Nouvelle commande
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<%= render 'components/breadcrumb', crumbs: [
|
||||
{ name: 'Accueil', path: root_path },
|
||||
{ name: 'Événements', path: events_path },
|
||||
{ name: @event.name, path: event_path(@event.slug, @event) },
|
||||
{ name: 'Nouvelle commande', path: nil }
|
||||
] %>
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8">
|
||||
@@ -130,7 +107,7 @@
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 pt-6">
|
||||
<%= link_to "Retour", event_path(@event.slug, @event), class: "px-6 py-3 border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 text-center font-medium transition-colors duration-200" %>
|
||||
<%= form.submit "Procéder au paiement", class: "flex-1 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-medium py-3 px-6 rounded-xl shadow-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transform hover:-translate-y-0.5" %>
|
||||
<%= form.button "Procéder au paiement", type: "submit", class: "flex-1 btn btn-primary py-3 px-6 rounded-xl shadow-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transform hover:-translate-y-0.5" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
198
app/views/orders/payment_cancel.html.erb
Normal file
198
app/views/orders/payment_cancel.html.erb
Normal file
@@ -0,0 +1,198 @@
|
||||
<div class="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-50 py-8">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="flex mb-6" aria-label="Breadcrumb">
|
||||
<ol class="inline-flex items-center space-x-1 md:space-x-2 rounded-lg bg-white px-4 py-2 shadow-sm">
|
||||
<li class="inline-flex items-center">
|
||||
<%= link_to "Accueil", root_path, class: "inline-flex items-center text-sm font-medium text-gray-700 hover:text-purple-600" %>
|
||||
</li>
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<%= link_to "Tableau de bord", dashboard_path, class: "ml-1 text-sm font-medium text-gray-700 hover:text-purple-600 md:ml-2" %>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span class="ml-1 text-sm font-medium text-purple-600 md:ml-2">Commande #<%= @order&.id || 'Inconnue' %></span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="mx-auto w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mb-4">
|
||||
<svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Détails de la Commande</h1>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Event & Order Details -->
|
||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-2">Détails de Votre Commande</h2>
|
||||
<% if @order %>
|
||||
<div class="flex items-center text-sm text-gray-600 space-x-4">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">Commande n°<%= @order.id %></span>
|
||||
<span class="text-xs text-gray-500"><%= @order.created_at.strftime("%d %B %Y") %></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span class="text-red-600 font-medium">
|
||||
Paiement annulé
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-gray-600">Aucune commande trouvée.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if @order %>
|
||||
<!-- Event Information -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-3">Événement</h3>
|
||||
<div class="bg-purple-50 rounded-lg p-4 border border-purple-200">
|
||||
<h4 class="font-semibold text-purple-900 text-lg"><%= @order.event.name %></h4>
|
||||
<div class="mt-2 space-y-1 text-sm text-purple-700">
|
||||
<% if @order.event.start_time %>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" 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"/>
|
||||
</svg>
|
||||
<%= @order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @order.event.venue_name.present? %>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
<%= @order.event.venue_name %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @order.event.venue_address.present? %>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/>
|
||||
</svg>
|
||||
<%= @order.event.venue_address %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Récapitulatif</h3>
|
||||
|
||||
<% @order.tickets.each do |ticket| %>
|
||||
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="text-sm font-medium text-gray-900 truncate"><%= ticket.ticket_type.name %></h4>
|
||||
<div class="flex items-center text-xs text-gray-500 mt-1">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
<%= ticket.first_name %> <%= ticket.last_name %>
|
||||
</div>
|
||||
<div class="flex items-center text-xs text-red-600 mt-1">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
En attente de paiement
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-lg font-semibold text-gray-900"><%= ticket.price_euros %>€</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Total -->
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600">Sous-total</span>
|
||||
<span class="text-gray-900"><%= @order.total_amount_euros - 1.0 %>€</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600">Frais de service</span>
|
||||
<span class="text-gray-900">1.00€</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-lg pt-2 border-t border-gray-200">
|
||||
<span class="font-medium text-gray-900">Total à payer</span>
|
||||
<span class="font-bold text-2xl text-red-600">
|
||||
<%= @order.total_amount_euros %>€
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Actions & Ticket Access -->
|
||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8 h-fit">
|
||||
<% if @order&.can_retry_payment? %>
|
||||
<!-- Payment Required -->
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-2">Paiement Requis</h2>
|
||||
<p class="text-sm text-gray-600">Votre commande nécessite un paiement</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<%= link_to checkout_order_path(@order), class: "block w-full text-center py-3 px-4 bg-orange-600 hover:bg-orange-700 text-white font-medium rounded-lg transition-colors" do %>
|
||||
<div class="flex items-center justify-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>
|
||||
</svg>
|
||||
Procéder au Paiement
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Navigation Actions -->
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="space-y-3">
|
||||
<%= link_to dashboard_path, class: "block w-full text-center py-3 px-4 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors" do %>
|
||||
<div class="flex items-center justify-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
Retour au Tableau de Bord
|
||||
</div>
|
||||
<% end %>
|
||||
<%= link_to event_path(@order.event.slug, @order.event), class: "block w-full text-center py-3 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors" do %>
|
||||
<div class="flex items-center justify-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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"/>
|
||||
</svg>
|
||||
Voir l'Événement Complet
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,35 +1,61 @@
|
||||
<div class="min-h-screen bg-gradient-to-br from-green-50 to-emerald-50 py-8">
|
||||
<div class="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-50 py-8">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Success Header -->
|
||||
<div class="text-center mb-12">
|
||||
<div class="mx-auto w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mb-6">
|
||||
<svg class="w-10 h-10 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="text-4xl font-bold text-gray-900 mb-4">Paiement réussi !</h1>
|
||||
<p class="text-xl text-gray-600 max-w-2xl mx-auto">
|
||||
Félicitations ! Votre commande a été traitée avec succès. Vous allez recevoir vos billets par email d'ici quelques minutes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Order Summary -->
|
||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-2">Récapitulatif de la commande</h2>
|
||||
<div class="flex items-center text-sm text-gray-600 space-x-4">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="flex mb-6" aria-label="Breadcrumb">
|
||||
<ol class="inline-flex items-center space-x-1 md:space-x-2 rounded-lg bg-white px-4 py-2 shadow-sm">
|
||||
<li class="inline-flex items-center">
|
||||
<%= link_to "Accueil", root_path, class: "inline-flex items-center text-sm font-medium text-gray-700 hover:text-purple-600" %>
|
||||
</li>
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<%= link_to "Tableau de bord", dashboard_path, class: "ml-1 text-sm font-medium text-gray-700 hover:text-purple-600 md:ml-2" %>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span class="ml-1 text-sm font-medium text-purple-600 md:ml-2">Commande #<%= @order.id %></span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="mx-auto w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mb-4">
|
||||
<svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
Commande #<%= @order.id %>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Détails de la Commande</h1>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Event & Order Details -->
|
||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-2">Détails de Votre Commande</h2>
|
||||
<div class="flex items-center text-sm text-gray-600 space-x-4">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">Commande n°<%= @order.id %></span>
|
||||
<span class="text-xs text-gray-500"><%= @order.created_at.strftime("%d %B %Y") %></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="text-green-600 font-medium">Payée</span>
|
||||
<span class="text-green-600 font-medium">
|
||||
Payée
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,7 +71,7 @@
|
||||
<svg class="w-4 h-4 mr-2" 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"/>
|
||||
</svg>
|
||||
<%= l(@order.event.start_time, format: :long) %>
|
||||
<%= @order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @order.event.venue_name.present? %>
|
||||
@@ -57,13 +83,21 @@
|
||||
<%= @order.event.venue_name %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @order.event.venue_address.present? %>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/>
|
||||
</svg>
|
||||
<%= @order.event.venue_address %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tickets List -->
|
||||
<!-- Summary -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Vos billets</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Récapitulatif</h3>
|
||||
|
||||
<% @order.tickets.each do |ticket| %>
|
||||
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0">
|
||||
@@ -91,48 +125,59 @@
|
||||
|
||||
<!-- Total -->
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="flex items-center justify-between text-lg">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600">Sous-total</span>
|
||||
<span class="text-gray-900"><%= @order.total_amount_euros - 1.0 %>€</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600">Frais de service</span>
|
||||
<span class="text-gray-900">1.00€</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-lg pt-2 border-t border-gray-200">
|
||||
<span class="font-medium text-gray-900">Total payé</span>
|
||||
<span class="font-bold text-2xl text-green-600"><%= @order.total_amount_euros %>€</span>
|
||||
<span class="font-bold text-2xl text-green-600">
|
||||
<%= @order.total_amount_euros %>€
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Next Steps -->
|
||||
<!-- Actions & Ticket Access -->
|
||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8 h-fit">
|
||||
<!-- Ticket Access -->
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-2">Prochaines étapes</h2>
|
||||
<p class="text-sm text-gray-600">Que faire maintenant ?</p>
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-2">Accédez à Vos Billets</h2>
|
||||
<p class="text-sm text-gray-600">Téléchargez ou consultez vos billets</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Email Confirmation -->
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<span class="text-blue-600 font-semibold text-sm">1</span>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="font-semibold text-gray-900 mb-1">Vérifiez votre email</h3>
|
||||
<p class="text-gray-600 text-sm">Nous avons envoyé vos billets à <strong><%= current_user.email %></strong>. Vérifiez aussi vos spams.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Download Tickets -->
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<span class="text-purple-600 font-semibold text-sm">2</span>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="font-semibold text-gray-900 mb-1">Téléchargez vos billets</h3>
|
||||
<p class="text-gray-600 text-sm mb-3">Gardez vos billets sur votre téléphone ou imprimez-les.</p>
|
||||
<div class="space-y-2">
|
||||
<% @order.tickets.each do |ticket| %>
|
||||
<%= link_to download_ticket_path(ticket), class: "inline-flex items-center px-3 py-2 border border-purple-300 rounded-md text-sm font-medium text-purple-700 bg-purple-50 hover:bg-purple-100 transition-colors mr-2 mb-2" do %>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="font-semibold text-gray-900 mb-1">Télécharger Vos Billets</h3>
|
||||
<p class="text-gray-600 text-sm mb-3">Gardez vos billets sur votre téléphone ou imprimez-les.</p>
|
||||
<div class="space-y-2">
|
||||
<% @order.tickets.each_with_index do |ticket, index| %>
|
||||
<div class="flex items-center justify-between p-3 border border-purple-200 rounded-lg bg-purple-50 hover:bg-purple-100 transition-colors">
|
||||
<%= link_to ticket_path(ticket.qr_code), class: "flex-1 flex items-center text-purple-700 hover:text-purple-800 font-medium" do %>
|
||||
<div class="flex items-center justify-center w-6 h-6 bg-purple-200 text-purple-800 text-xs font-bold rounded-full mr-3">
|
||||
<%= index + 1 %>
|
||||
</div>
|
||||
<%= ticket.first_name %> <%= ticket.last_name %>
|
||||
<% end %>
|
||||
<%= link_to ticket_download_path(ticket.qr_code), class: "ml-3 p-2 text-purple-600 hover:text-purple-800 hover:bg-purple-200 rounded-lg transition-colors", title: "Télécharger le billet PDF" do %>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,46 +186,34 @@
|
||||
<!-- Event Day -->
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<span class="text-green-600 font-semibold text-sm">3</span>
|
||||
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="font-semibold text-gray-900 mb-1">Le jour J</h3>
|
||||
<h3 class="font-semibold text-gray-900 mb-1">Le Jour de l'Événement</h3>
|
||||
<p class="text-gray-600 text-sm">Présentez votre billet (QR code) à l'entrée. Arrivez un peu en avance !</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Support -->
|
||||
<div class="bg-gray-50 rounded-lg p-4 mt-8">
|
||||
<h4 class="font-medium text-gray-900 mb-2">Besoin d'aide ?</h4>
|
||||
<p class="text-gray-600 text-sm mb-3">Si vous avez des questions ou des problèmes avec votre commande, n'hésitez pas à nous contacter.</p>
|
||||
<div class="space-y-2">
|
||||
<%= link_to "mailto:support@example.com", class: "inline-flex items-center text-sm text-purple-600 hover:text-purple-700" do %>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
Contactez le support
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<!-- Navigation Actions -->
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="space-y-3">
|
||||
<%= link_to dashboard_path, class: "block w-full text-center py-3 px-4 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors" do %>
|
||||
<div class="flex items-center justify-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
Voir tous mes billets
|
||||
Retour au Tableau de Bord
|
||||
</div>
|
||||
<% end %>
|
||||
<%= link_to events_path, class: "block w-full text-center py-3 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors" do %>
|
||||
<%= link_to event_path(@order.event.slug, @order.event), class: "block w-full text-center py-3 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors" do %>
|
||||
<div class="flex items-center justify-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
<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"/>
|
||||
</svg>
|
||||
Découvrir d'autres événements
|
||||
Voir l'Événement Complet
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -1,66 +1,91 @@
|
||||
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 py-8">
|
||||
<div class="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-50 py-8">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="mb-8" aria-label="Breadcrumb">
|
||||
<ol class="flex items-center space-x-2 text-sm">
|
||||
<%= link_to root_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
</svg>
|
||||
Accueil
|
||||
<% end %>
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<%= link_to events_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
Événements
|
||||
<% end %>
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<%= link_to event_path(@order.event.slug, @order.event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<%= @order.event.name %>
|
||||
<% end %>
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<li class="font-medium text-gray-900" aria-current="page">Commande #<%= @order.id %></li>
|
||||
</ol>
|
||||
</nav>
|
||||
<%= render 'components/breadcrumb', crumbs: [
|
||||
{ name: 'Accueil', path: root_path },
|
||||
{ name: 'Tableau de bord', path: dashboard_path },
|
||||
{ name: "Commande ##{@order.id}", path: nil }
|
||||
] %>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="mx-auto w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mb-4">
|
||||
<i data-lucide="file-text" class="w-8 h-8 text-purple-600"></i>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Détails de la Commande</h1>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Event & Order Details -->
|
||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-2">Détails de la commande</h1>
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-2">Informations</h2>
|
||||
<div class="flex items-center text-sm text-gray-600 space-x-4">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
Commande #<%= @order.id %>
|
||||
<i data-lucide="file-text" class="w-4 h-4 mr-2 flex-shrink-0"></i>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">Commande n°<%= @order.id %></span>
|
||||
<span class="text-xs text-gray-500"><%= @order.created_at.strftime("%d %B %Y") %></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" 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"/>
|
||||
</svg>
|
||||
<%= @order.status.titleize %>
|
||||
<i data-lucide="<%= @order.status == 'paid' || @order.status == 'completed' ? 'check-circle' : 'clock' %>" class="w-4 h-4 mr-1 <%= @order.status == 'paid' || @order.status == 'completed' ? 'text-green-600' : 'text-yellow-600' %>"></i>
|
||||
<span class="<%= @order.status == 'paid' || @order.status == 'completed' ? 'text-green-600' : 'text-yellow-600' %> font-medium">
|
||||
<%= case @order.status
|
||||
when 'paid' then 'Payé'
|
||||
when 'completed' then 'Terminé'
|
||||
else @order.status.humanize
|
||||
end %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Order Items -->
|
||||
<div class="space-y-4 mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Billets commandés</h3>
|
||||
|
||||
<!-- Event Information -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-3">Événement</h3>
|
||||
<div class="bg-purple-50 rounded-lg p-4 border border-purple-200">
|
||||
<h4 class="font-semibold text-purple-900 text-lg"><%= @order.event.name %></h4>
|
||||
<div class="mt-2 space-y-1 text-sm text-purple-700">
|
||||
<% if @order.event.start_time %>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="clock" class="w-4 h-4 mr-2"></i>
|
||||
<%= @order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @order.event.venue_name.present? %>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="map-pin" class="w-4 h-4 mr-2"></i>
|
||||
<%= @order.event.venue_name %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @order.event.venue_address.present? %>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="navigation" class="w-4 h-4 mr-2"></i>
|
||||
<%= @order.event.venue_address %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Récapitulatif</h3>
|
||||
|
||||
<% @tickets.each do |ticket| %>
|
||||
<div class="flex items-center justify-between py-4 border-b border-gray-100 last:border-b-0">
|
||||
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="text-sm font-medium text-gray-900"><%= ticket.ticket_type.name %></h4>
|
||||
<h4 class="text-sm font-medium text-gray-900 truncate"><%= ticket.ticket_type.name %></h4>
|
||||
<div class="flex items-center text-xs text-gray-500 mt-1">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
<i data-lucide="user" class="w-3 h-3 mr-1"></i>
|
||||
<%= ticket.first_name %> <%= ticket.last_name %>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
Statut: <%= ticket.status.titleize %>
|
||||
<% if @order.status == 'paid' || @order.status == 'completed' %>
|
||||
<div class="flex items-center text-xs text-green-600 mt-1">
|
||||
<i data-lucide="check-circle" class="w-3 h-3 mr-1"></i>
|
||||
Actif
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-lg font-semibold text-gray-900"><%= ticket.price_euros %>€</div>
|
||||
@@ -68,35 +93,131 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<!-- Order Total -->
|
||||
<div class="border-t border-gray-200 pt-6">
|
||||
<div class="flex items-center justify-between text-lg">
|
||||
<span class="font-medium text-gray-900">Total</span>
|
||||
<span class="font-bold text-2xl text-purple-600"><%= @order.total_amount_euros %>€</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">TVA incluse</p>
|
||||
</div>
|
||||
<!-- Actions -->
|
||||
|
||||
<!-- Total -->
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="flex space-x-4">
|
||||
<%= link_to event_path(@order.event.slug, @order.event), class: "bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-2 px-4 rounded-lg transition-colors" do %>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
Retour à l'événement
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600">Sous-total</span>
|
||||
<span class="text-gray-900"><%= @order.total_amount_euros - 1.0 %>€</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600">Frais de service</span>
|
||||
<span class="text-gray-900">1.00€</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-lg pt-2 border-t border-gray-200">
|
||||
<span class="font-medium text-gray-900">Total <%= @order.status == 'paid' || @order.status == 'completed' ? 'payé' : 'à payer' %></span>
|
||||
<span class="font-bold text-2xl <%= @order.status == 'paid' || @order.status == 'completed' ? 'text-green-600' : 'text-purple-600' %>">
|
||||
<%= @order.total_amount_euros %>€
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- View Invoice -->
|
||||
<% if @order.status == 'paid' || @order.status == 'completed' %>
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<i data-lucide="file-text" class="w-4 h-4 text-blue-600"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="font-semibold text-gray-900 mb-1">Consulter la Facture</h3>
|
||||
<p class="text-gray-600 text-sm mb-3">Téléchargez ou consultez la facture de votre commande.</p>
|
||||
<div class="mt-2">
|
||||
<%= link_to invoice_order_path(@order), class: "inline-flex items-center px-4 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors" do %>
|
||||
<i data-lucide="file-text" class="w-4 h-4 mr-2"></i>
|
||||
Voir la facture
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Actions & Ticket Access -->
|
||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8 h-fit">
|
||||
<% if @order.status == 'paid' || @order.status == 'completed' %>
|
||||
<!-- Ticket Access -->
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-2">Accédez à Vos Billets</h2>
|
||||
<p class="text-sm text-gray-600">Téléchargez ou consultez vos billets</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Download Tickets -->
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<i data-lucide="download" class="w-4 h-4 text-purple-600"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="font-semibold text-gray-900 mb-1">Télécharger Vos Billets</h3>
|
||||
<p class="text-gray-600 text-sm mb-3">Gardez vos billets sur votre téléphone ou imprimez-les.</p>
|
||||
<div class="space-y-2">
|
||||
<% @tickets.each_with_index do |ticket, index| %>
|
||||
<div class="flex items-center justify-between p-3 border border-purple-200 rounded-lg bg-purple-50 hover:bg-purple-100 transition-colors">
|
||||
<%= link_to ticket_path(ticket.qr_code), class: "flex-1 flex items-center text-purple-700 hover:text-purple-800 font-medium" do %>
|
||||
<div class="flex items-center justify-center w-6 h-6 bg-purple-200 text-purple-800 text-xs font-bold rounded-full mr-3">
|
||||
<%= index + 1 %>
|
||||
</div>
|
||||
<%= ticket.first_name %> <%= ticket.last_name %>
|
||||
<% end %>
|
||||
<%= link_to ticket_download_path(ticket.qr_code), class: "ml-3 p-2 text-purple-600 hover:text-purple-800 hover:bg-purple-200 rounded-lg transition-colors", title: "Télécharger le billet PDF" do %>
|
||||
<i data-lucide="download" class="w-4 h-4"></i>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Day -->
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<i data-lucide="check-circle" class="w-4 h-4 text-green-600"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="font-semibold text-gray-900 mb-1">Le Jour de l'Événement</h3>
|
||||
<p class="text-gray-600 text-sm">Présentez votre billet (QR code) à l'entrée. Arrivez un peu en avance !</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<!-- Payment Required -->
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-2">Paiement Requis</h2>
|
||||
<p class="text-sm text-gray-600">Votre commande nécessite un paiement</p>
|
||||
</div>
|
||||
|
||||
<% if @order.can_retry_payment? %>
|
||||
<%= link_to checkout_order_path(@order), class: "bg-purple-600 hover:bg-purple-700 text-white font-medium py-2 px-4 rounded-lg transition-colors" do %>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>
|
||||
</svg>
|
||||
Procéder au paiement
|
||||
<div class="mb-6">
|
||||
<%= link_to checkout_order_path(@order), class: "block w-full text-center py-3 px-4 bg-orange-600 hover:bg-orange-700 text-white font-medium rounded-lg transition-colors" do %>
|
||||
<div class="flex items-center justify-center">
|
||||
<i data-lucide="credit-card" class="w-4 h-4 mr-2"></i>
|
||||
Procéder au Paiement
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<!-- Navigation Actions -->
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="space-y-3">
|
||||
<%= link_to dashboard_path, class: "block w-full text-center py-3 px-4 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors" do %>
|
||||
<div class="flex items-center justify-center">
|
||||
<i data-lucide="arrow-left" class="w-4 h-4 mr-2"></i>
|
||||
Retour au tableau de bord
|
||||
</div>
|
||||
<% end %>
|
||||
<%= link_to event_path(@order.event.slug, @order.event), class: "block w-full text-center py-3 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors" do %>
|
||||
<div class="flex items-center justify-center">
|
||||
<i data-lucide="calendar" class="w-4 h-4 mr-2"></i>
|
||||
Voir la page d'évenement
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,74 +1,234 @@
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Hero section with metrics -->
|
||||
<div class="mt-4 mb-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-slate-100">Tableau de bord</h1>
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<!-- Breadcrumb -->
|
||||
<%= render 'components/breadcrumb', crumbs: [
|
||||
{ name: 'Accueil', path: root_path },
|
||||
{ name: 'Tableau de bord', path: dashboard_path }
|
||||
] %>
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">Mon tableau de bord</h1>
|
||||
<p class="text-gray-600 mt-1">Gérez vos commandes et accédez à vos billets</p>
|
||||
</div>
|
||||
|
||||
<!-- Promoter Actions -->
|
||||
<% if current_user.promoter? %>
|
||||
<div class="flex items-center space-x-3">
|
||||
<%= link_to promoter_events_path, class: "inline-flex items-center px-4 py-2 bg-purple-600 text-white font-medium rounded-lg hover:bg-purple-700 transition-colors duration-200" do %>
|
||||
<div class="flex flex-col xs:flex-row items-stretch xs:items-center gap-2">
|
||||
<%= link_to promoter_events_path, class: "inline-flex items-center justify-center px-4 py-2 bg-purple-600 text-white font-medium rounded-lg hover:bg-purple-700 transition-colors duration-200" do %>
|
||||
<i data-lucide="calendar-plus" class="w-4 h-4 mr-2"></i>
|
||||
Mes événements
|
||||
Mes Événements
|
||||
<% end %>
|
||||
<%= link_to new_promoter_event_path, class: "inline-flex items-center px-4 py-2 bg-black text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" do %>
|
||||
<%= link_to new_promoter_event_path, class: "inline-flex items-center justify-center px-4 py-2 bg-gray-900 text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" do %>
|
||||
<i data-lucide="plus" class="w-4 h-4 mr-2"></i>
|
||||
Créer un événement
|
||||
Créer un Événement
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= link_to events_path, class: "inline-flex items-center justify-center px-4 py-2 bg-purple-600 text-white font-medium rounded-lg hover:bg-purple-700 transition-colors duration-200" do %>
|
||||
<i data-lucide="search" class="w-4 h-4 mr-2"></i>
|
||||
Découvrir des Événements
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Promoter Dashboard Section -->
|
||||
<% if current_user.promoter? && @promoter_events.present? %>
|
||||
<!-- Promoter Metrics -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-gradient-to-br from-green-50 to-green-100 rounded-2xl p-6 border border-green-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-green-600 text-sm font-medium">Revenus Total</p>
|
||||
<p class="text-2xl font-bold text-green-900">€<%= number_with_delimiter(@total_revenue, delimiter: ' ') %></p>
|
||||
</div>
|
||||
<div class="bg-green-200 rounded-full p-3">
|
||||
<i data-lucide="euro" class="w-6 h-6 text-green-700"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-blue-50 to-blue-100 rounded-2xl p-6 border border-blue-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-blue-600 text-sm font-medium">Billets Vendus</p>
|
||||
<p class="text-2xl font-bold text-blue-900"><%= @total_tickets_sold %></p>
|
||||
</div>
|
||||
<div class="bg-blue-200 rounded-full p-3">
|
||||
<i data-lucide="ticket" class="w-6 h-6 text-blue-700"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-purple-50 to-purple-100 rounded-2xl p-6 border border-purple-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-purple-600 text-sm font-medium">Événements Publiés</p>
|
||||
<p class="text-2xl font-bold text-purple-900"><%= @active_events_count %></p>
|
||||
</div>
|
||||
<div class="bg-purple-200 rounded-full p-3">
|
||||
<i data-lucide="calendar-check" class="w-6 h-6 text-purple-700"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-orange-50 to-orange-100 rounded-2xl p-6 border border-orange-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-orange-600 text-sm font-medium">Brouillons</p>
|
||||
<p class="text-2xl font-bold text-orange-900"><%= @draft_events_count %></p>
|
||||
</div>
|
||||
<div class="bg-orange-200 rounded-full p-3">
|
||||
<i data-lucide="edit-3" class="w-6 h-6 text-orange-700"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Revenue Chart & Recent Events -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
|
||||
<!-- Monthly Revenue Chart -->
|
||||
<div class="lg:col-span-2 bg-white rounded-2xl shadow-lg">
|
||||
<div class="border-b border-gray-100 p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900">Revenus Mensuels</h2>
|
||||
<p class="text-gray-600 mt-1">Derniers 6 mois</p>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="space-y-3">
|
||||
<% @monthly_revenue.each do |month_data| %>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700"><%= month_data[:month] %></span>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-32 bg-gray-200 rounded-full h-3 relative">
|
||||
<div class="bg-green-500 h-3 rounded-full" style="width: <%= [month_data[:revenue] / ([@monthly_revenue.max_by{|m| m[:revenue]}[:revenue], 1].max) * 100, 5].max %>%"></div>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-gray-900 w-16 text-right">€<%= number_with_delimiter(month_data[:revenue], delimiter: ' ') %></span>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
|
||||
<%= render partial: 'components/metric_card', locals: { title: "Mes réservations", value: @booked_events, classes: "from-green-100 to-emerald-100" } %>
|
||||
|
||||
<%= render partial: 'components/metric_card', locals: { title: "Événements aujourd'hui", value: @events_today, classes: "from-blue-100 to-sky-100" } %>
|
||||
|
||||
<%= render partial: 'components/metric_card', locals: { title: "Événements demain", value: @events_tomorrow, classes: "from-purple-100 to-indigo-100" } %>
|
||||
|
||||
<%= render partial: 'components/metric_card', locals: { title: "À venir", value: @upcoming_events, classes: "from-orange-100 to-amber-100" } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Events -->
|
||||
<div class="bg-white rounded-2xl shadow-lg">
|
||||
<div class="border-b border-gray-100 p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-bold text-gray-900">Mes Événements</h2>
|
||||
<%= link_to promoter_events_path, class: "text-purple-600 hover:text-purple-800 font-medium text-sm" do %>
|
||||
Voir tout →
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="space-y-4">
|
||||
<% @promoter_events.each do |event| %>
|
||||
<div class="border border-gray-200 rounded-xl p-4 hover:shadow-md transition-shadow">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<h4 class="font-semibold text-gray-900 text-sm"><%= event.name %></h4>
|
||||
<span class="text-xs px-2 py-1 rounded-full <%= event.state == 'published' ? 'bg-green-100 text-green-800' : event.state == 'draft' ? 'bg-yellow-100 text-yellow-800' : 'bg-red-100 text-red-800' %>">
|
||||
<%= event.state.humanize %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 space-y-1">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="calendar" class="w-3 h-3 mr-2"></i>
|
||||
<%= event.start_time&.strftime("%d %B %Y") || "Non programmé" %>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="ticket" class="w-3 h-3 mr-2"></i>
|
||||
<%= event.tickets.where(status: 'active').count %> billets vendus
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex space-x-2">
|
||||
<%= link_to promoter_event_path(event), class: "text-purple-600 hover:text-purple-800 text-xs font-medium" do %>
|
||||
Gérer →
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="mt-4 text-center">
|
||||
<%= link_to new_promoter_event_path, class: "inline-flex items-center px-4 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors" do %>
|
||||
<i data-lucide="plus" class="w-4 h-4 mr-2"></i>
|
||||
Nouvel Événement
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Orders -->
|
||||
<% if @recent_orders.any? %>
|
||||
<div class="bg-white rounded-2xl shadow-lg mb-8">
|
||||
<div class="border-b border-gray-100 p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900">Commandes Récentes</h2>
|
||||
<p class="text-gray-600 mt-1">Dernières commandes pour vos événements</p>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="text-left border-b border-gray-200">
|
||||
<th class="pb-3 text-sm font-medium text-gray-600">Événement</th>
|
||||
<th class="pb-3 text-sm font-medium text-gray-600">Client</th>
|
||||
<th class="pb-3 text-sm font-medium text-gray-600">Billets</th>
|
||||
<th class="pb-3 text-sm font-medium text-gray-600">Montant</th>
|
||||
<th class="pb-3 text-sm font-medium text-gray-600">Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
<% @recent_orders.each do |order| %>
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="py-3 text-sm font-medium text-gray-900"><%= order.event.name %></td>
|
||||
<td class="py-3 text-sm text-gray-700"><%= order.user.email %></td>
|
||||
<td class="py-3 text-sm text-gray-700"><%= order.tickets.count %></td>
|
||||
<td class="py-3 text-sm font-medium text-gray-900">€<%= order.total_amount_euros %></td>
|
||||
<td class="py-3 text-sm text-gray-500"><%= order.created_at.strftime("%d/%m/%Y") %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<!-- Draft orders needing payment -->
|
||||
<% if @draft_orders.any? %>
|
||||
<div class="card hover-lift mb-8 border-orange-200 bg-orange-50">
|
||||
<div class="card-header bg-orange-100 rounded-lg">
|
||||
|
||||
<div class="mx-4 py-4">
|
||||
<h2 class="text-2xl font-bold text-orange-900 flex items-center">
|
||||
<svg class="w-6 h-6 mr-2 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
Commandes en attente de paiement
|
||||
<div class="bg-orange-50 border border-orange-200 rounded-2xl shadow-lg mb-8">
|
||||
<div class="bg-orange-100 rounded-t-2xl p-4 sm:p-6">
|
||||
<h2 class="text-xl sm:text-2xl font-bold text-orange-900 flex items-center">
|
||||
<i data-lucide="alert-triangle" class="w-5 h-5 sm:w-6 sm:h-6 mr-2 text-orange-600"></i>
|
||||
Commandes en Attente de Paiement
|
||||
</h2>
|
||||
<p class="text-orange-700 mt-1">Vous avez des commandes qui nécessitent un paiement</p>
|
||||
<p class="text-gray-700 mt-1">Vous avez des commandes qui nécessitent un paiement</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="p-4 sm:p-6">
|
||||
<div class="space-y-4">
|
||||
<% @draft_orders.each do |order| %>
|
||||
<div class="bg-white rounded-lg p-4 border border-orange-200">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="bg-white rounded-xl p-4 border border-orange-200">
|
||||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3 mb-3">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900"><%= order.event.name %></h3>
|
||||
<p class="text-sm text-gray-600">
|
||||
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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"/>
|
||||
</svg>
|
||||
<h3 class="font-semibold text-gray-900 text-base sm:text-lg"><%= order.event.name %></h3>
|
||||
<p class="text-sm text-gray-600 mt-1 flex items-center">
|
||||
<i data-lucide="calendar" class="w-4 h-4 mr-2"></i>
|
||||
<%= order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-orange-600 bg-orange-100 px-2 py-1 rounded-full">
|
||||
Commande #<%= order.id %>
|
||||
<span class="text-sm font-medium text-orange-600 bg-orange-100 px-3 py-1 rounded-full whitespace-nowrap">
|
||||
Order #<%= order.id %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2 mb-4">
|
||||
<% order.tickets.each do |ticket| %>
|
||||
<div class="flex items-center justify-between text-sm bg-gray-50 rounded p-2">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between text-sm bg-gray-50 rounded-lg p-3 gap-2">
|
||||
<div>
|
||||
<span class="font-medium"><%= ticket.ticket_type.name %></span>
|
||||
<span class="text-gray-600">- <%= ticket.first_name %> <%= ticket.last_name %></span>
|
||||
@@ -80,19 +240,22 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||
<div class="text-sm text-gray-600">
|
||||
<div class="mb-1 sm:mb-0">
|
||||
Tentatives: <%= order.payment_attempts %>/3
|
||||
</div>
|
||||
<% if order.expiring_soon? %>
|
||||
<span class="text-orange-600 font-medium ml-2">⚠️ Expire dans <%= time_ago_in_words(order.expires_at) %></span>
|
||||
<span class="text-orange-600 font-medium">⚠️ Expire dans <%= time_ago_in_words(order.expires_at) %></span>
|
||||
<% else %>
|
||||
<span class="text-gray-500 ml-2">Expire dans <%= time_ago_in_words(order.expires_at) %></span>
|
||||
<span class="text-gray-500">Expire dans <%= time_ago_in_words(order.expires_at) %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= link_to retry_payment_order_path(order), method: :post,
|
||||
class: "inline-flex items-center px-4 py-2 bg-orange-600 text-white text-sm font-medium rounded-lg hover:bg-orange-700 transition-colors duration-200" do %>
|
||||
Reprendre le paiement (<%= order.total_amount_euros %>€)
|
||||
class: "inline-flex items-center px-4 py-2 bg-orange-600 text-white text-sm font-medium rounded-lg hover:bg-orange-700 transition-colors duration-200 whitespace-nowrap" do %>
|
||||
<i data-lucide="credit-card" class="w-4 h-4 mr-2"></i>
|
||||
Reprendre le Paiement (€<%= order.total_amount_euros %>)
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,96 +265,148 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- User's booked 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">Mes événements réservés</h2>
|
||||
<!-- User's Orders Section -->
|
||||
<div class="bg-white rounded-2xl shadow-lg mb-8">
|
||||
<div class="border-b border-gray-100 p-4 sm:p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl sm:text-2xl font-bold text-gray-900">Mes Commandes</h2>
|
||||
<span class="text-sm text-gray-600 bg-gray-100 px-3 py-1 rounded-full">
|
||||
<%= pluralize(@user_orders.count, 'commande') %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if @user_booked_events.any? %>
|
||||
<ul class="space-y-4">
|
||||
<% @user_booked_events.each do |event| %>
|
||||
<li>
|
||||
<%= render partial: 'components/event_item', locals: { event: event } %>
|
||||
</li>
|
||||
</div>
|
||||
<div class="p-4 sm:p-6">
|
||||
<% if @user_orders.any? %>
|
||||
<div class="space-y-4">
|
||||
<% @user_orders.each do |order| %>
|
||||
<div class="bg-gray-50 rounded-xl p-4 hover:shadow-md transition-shadow">
|
||||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3 mb-3">
|
||||
<div class="flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2 mb-2">
|
||||
<h3 class="font-semibold text-gray-900 text-base sm:text-lg"><%= order.event.name %></h3>
|
||||
<span class="text-xs px-2 py-1 rounded-full <%= order.status == 'paid' ? 'bg-green-100 text-green-800' : order.status == 'completed' ? 'bg-blue-100 text-blue-800' : 'bg-yellow-100 text-yellow-800' %>">
|
||||
<%= order.status.humanize %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3 text-sm text-gray-600 mb-2">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="calendar" class="w-4 h-4 mr-2"></i>
|
||||
<%= order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="map-pin" class="w-4 h-4 mr-2"></i>
|
||||
<%= order.event.venue_name %>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="shopping-bag" class="w-4 h-4 mr-2"></i>
|
||||
<%= pluralize(order.tickets.count, 'billet') %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-gray-500 mt-2">
|
||||
Order #<%= order.id %> • <%= order.created_at.strftime("%m/%d/%Y") %> • €<%= order.total_amount_euros %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<%= link_to order_path(order),
|
||||
class: "inline-flex items-center px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 whitespace-nowrap" do %>
|
||||
<i data-lucide="eye" class="w-4 h-4 mr-2"></i>
|
||||
Voir les Détails
|
||||
<% end %>
|
||||
</ul>
|
||||
<% if @booked_events > 5 %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick tickets preview -->
|
||||
<div class="border-t border-gray-200 pt-3">
|
||||
<div class="grid gap-2">
|
||||
<% order.tickets.limit(3).each do |ticket| %>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between text-sm bg-white rounded-lg p-3 gap-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full"></span>
|
||||
<span class="font-medium"><%= ticket.ticket_type.name %></span>
|
||||
<span class="text-gray-500 text-sm">- <%= ticket.first_name %> <%= ticket.last_name %></span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<%= link_to ticket_download_path(ticket.qr_code),
|
||||
class: "text-purple-600 hover:text-purple-800" do %>
|
||||
<i data-lucide="download" class="w-4 h-4"></i>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if order.tickets.count > 3 %>
|
||||
<div class="text-xs text-gray-500 text-center">
|
||||
et <%= pluralize(order.tickets.count - 3, 'autre billet') %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if @user_orders.count >= 10 %>
|
||||
<div class="mt-6 text-center">
|
||||
<%= link_to "Voir toutes mes réservations", "#", class: "text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 font-medium transition-colors duration-200" %>
|
||||
<%= link_to "Voir Toutes Mes Commandes", orders_path, class: "text-purple-600 hover:text-purple-800 font-medium transition-colors duration-200" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="text-center py-8">
|
||||
<p class="text-slate-600 dark:text-slate-400 mb-4">Vous n'avez encore réservé aucun événement.</p>
|
||||
<%= link_to "Découvrir les événements", events_path, class: "inline-flex items-center px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors duration-200" %>
|
||||
<div class="text-center py-12">
|
||||
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i data-lucide="shopping-bag" class="w-8 h-8 text-gray-400"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Aucune Commande</h3>
|
||||
<p class="text-gray-600 mb-6">Vous n'avez pas encore passé de commandes.</p>
|
||||
<%= link_to events_path, class: "inline-flex items-center px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors duration-200" do %>
|
||||
<i data-lucide="search" class="w-4 h-4 mr-2"></i>
|
||||
Découvrir des Événements
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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_events.any? %>
|
||||
<ul class="space-y-4">
|
||||
<% @today_events.each do |event| %>
|
||||
<li>
|
||||
<%= render partial: 'components/event_item', locals: { event: event } %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<p class="text-slate-600 dark:text-slate-400">Aucun évenement aujourd'hui.</p>
|
||||
<!-- Quick Events Preview -->
|
||||
<% if @user_orders.any? %>
|
||||
<div class="bg-white rounded-2xl shadow-lg">
|
||||
<div class="border-b border-gray-100 p-4 sm:p-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<h2 class="text-lg sm:text-xl font-bold text-gray-900">Découvrir d'autres événements</h2>
|
||||
<%= link_to events_path, class: "text-purple-600 hover:text-purple-800 font-medium transition-colors duration-200 whitespace-nowrap" do %>
|
||||
Voir tout →
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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 class="p-4 sm:p-6">
|
||||
<% if @upcoming_preview_events.any? %>
|
||||
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<% @upcoming_preview_events.each do |event| %>
|
||||
<div class="bg-gray-50 rounded-xl p-4 hover:shadow-md transition-shadow">
|
||||
<h4 class="font-medium text-gray-900 mb-2 text-base"><%= event.name %></h4>
|
||||
<div class="text-sm text-gray-600 space-y-1">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="calendar" class="w-4 h-4 mr-2"></i>
|
||||
<%= event.start_time.strftime("%d %B") %>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if @tomorrow_events.any? %>
|
||||
<ul class="space-y-4">
|
||||
<% @tomorrow_events.each do |event| %>
|
||||
<li>
|
||||
<%= render partial: 'components/event_item', locals: { event: event } %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<p class="text-slate-600 dark:text-slate-400">Aucune partie demain.</p>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="map-pin" class="w-4 h-4 mr-2"></i>
|
||||
<%= event.venue_name %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<%= link_to event_path(event.slug, event), class: "text-purple-600 hover:text-purple-800 text-sm font-medium" do %>
|
||||
Voir l'Événement →
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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_events.any? %>
|
||||
<ul class="space-y-4">
|
||||
<% @other_events.each do |event| %>
|
||||
<li>
|
||||
<%= render partial: 'components/event_item', locals: { event: event } %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="mt-8">
|
||||
<%= paginate @other_events %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-slate-600 dark:text-slate-400">Aucune autre partie à venir.</p>
|
||||
<p class="text-gray-600">Aucun événement à venir pour le moment.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -1,153 +1,248 @@
|
||||
<% content_for :title, "Aperonight - Découvrez des événements après-travail de luxe" %>
|
||||
<% content_for :title, "Aperonight - Découvrez les meilleurs événements après-travail" %>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<div class="hero-content">
|
||||
<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>
|
||||
<section class="relative bg-gradient-primary flex items-center overflow-hidden">
|
||||
<!-- Background overlay -->
|
||||
<div class="absolute inset-0 bg-black bg-opacity-30 z-10"></div>
|
||||
|
||||
<div class="cta-group">
|
||||
<%= 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 class="relative z-20 max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-16 lg:py-24">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
<!-- Hero Content -->
|
||||
<div class="text-center lg:text-left text-white">
|
||||
<h1 class="text-4xl lg:text-6xl font-bold mb-6 leading-tight">
|
||||
Découvrez les
|
||||
<span class="text-yellow-400">meilleurs événements</span>
|
||||
afterworks
|
||||
</h1>
|
||||
<p class="text-xl text-gray-200 mb-8 max-w-lg">
|
||||
Connectez-vous avec des professionnels et découvrez des événements exclusifs dans les plus beaux lieux de Paris.
|
||||
</p>
|
||||
|
||||
<!-- CTA Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start max-w-lg">
|
||||
<%= link_to events_path,
|
||||
class: "w-full sm:flex-1 bg-white text-gray-900 px-6 py-3 rounded-full font-semibold text-base hover:bg-gray-100 transition-all duration-200 inline-flex items-center justify-center shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" do %>
|
||||
<i data-lucide="calendar" class="w-4 h-4 mr-2"></i>
|
||||
Voir tous les événements
|
||||
<% end %>
|
||||
|
||||
<% unless user_signed_in? %>
|
||||
<%= link_to new_user_registration_path,
|
||||
class: "w-full sm:flex-1 border-2 border-white text-white px-6 py-3 rounded-full font-semibold text-base hover:bg-white hover:text-gray-900 transition-all duration-200 inline-flex items-center justify-center" do %>
|
||||
<i data-lucide="user-plus" class="w-4 h-4 mr-2"></i>
|
||||
Rejoindre gratuitement
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hero Visual -->
|
||||
<div class="relative">
|
||||
<div class="bg-gray-800 rounded-3xl p-8 shadow-2xl backdrop-blur-sm bg-opacity-90">
|
||||
<div class="text-center text-white">
|
||||
<div class="w-16 h-16 bg-yellow-400 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i data-lucide="calendar" class="w-8 h-8 text-gray-900"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold mb-2">Aperonight</h3>
|
||||
<p class="text-gray-300 mb-6">Événements premium après-travail</p>
|
||||
<div class="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-yellow-400"><%= @total_events %>+</div>
|
||||
<div class="text-sm text-gray-400">Événements</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-yellow-400"><%= (@total_users / 100.0).round(1) %>k</div>
|
||||
<div class="text-sm text-gray-400">Membres</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-yellow-400">5★</div>
|
||||
<div class="text-sm text-gray-400">Satisfaction</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%= 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">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>
|
||||
<section class="py-16 bg-white">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Section Header -->
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
|
||||
ÉVÉNEMENTS POPULAIRES À PARIS
|
||||
</h2>
|
||||
<p class="text-xl text-gray-600 max-w-2xl mx-auto">
|
||||
Découvrez une sélection d'événements après-travail soigneusement choisis dans les plus beaux lieux de la capitale.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="featured-events-grid" data-controller="featured-event">
|
||||
<!-- Events Grid -->
|
||||
<% if @featured_events.any? %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-12">
|
||||
<% @featured_events.each do |event| %>
|
||||
<div class="featured-event-card" data-featured-event-target="card">
|
||||
<%= link_to event_path(event.slug, event) do %>
|
||||
<img src="<%= event.image %>" alt="<%= event.name %>" class="featured-event-image" data-featured-event-target="animated">
|
||||
<div class="group cursor-pointer transform transition-all duration-300 hover:-translate-y-2">
|
||||
<%= link_to event_path(event.slug, event), class: "block" do %>
|
||||
<!-- Event Card -->
|
||||
<div class="bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 overflow-hidden">
|
||||
<!-- Event Image -->
|
||||
<div class="relative overflow-hidden aspect-[4/3]">
|
||||
<% if event.image.present? %>
|
||||
<img src="<%= event.image %>"
|
||||
alt="<%= event.name %>"
|
||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300">
|
||||
<% else %>
|
||||
<div class="w-full h-full bg-gradient-to-br from-purple-600 to-blue-600 flex items-center justify-center">
|
||||
<i data-lucide="calendar" class="w-16 h-16 text-white"></i>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="featured-event-content">
|
||||
<div class="featured-event-badges">
|
||||
|
||||
<!-- Event Badge -->
|
||||
<% if event.featured? %>
|
||||
<span class="badge badge-featured">★ En vedette</span>
|
||||
<div class="absolute top-4 left-4">
|
||||
<span class="bg-yellow-400 text-gray-900 px-3 py-1 rounded-full text-sm font-medium shadow-lg">
|
||||
★ En vedette
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if event.ticket_types.any? { |ticket_type| ticket_type.available_quantity > 0 } %>
|
||||
<!--<span class="badge badge-available">Disponible</span>-->
|
||||
<% end %>
|
||||
</div>
|
||||
<h3 class="featured-event-title"><%= event.name %></h3>
|
||||
<div class="featured-event-meta">
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="calendar"></i>
|
||||
<%= l(event.start_time, format: '%a, %b %d • %H:%M - %H:%M') %> <!-- Format: Wed, Jan 1 • 18:30 - 22:00 -->
|
||||
</div>
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="map-pin"></i>
|
||||
<%= event.venue_name %>, <%= event.venue_address %>
|
||||
</div>
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="users"></i>
|
||||
<%= event.tickets.sum(:quantity) %> participants • <%= event.tickets.joins(:ticket_type).where('ticket_types.quantity > ?', 0).count %> places disponibles
|
||||
</div>
|
||||
</div>
|
||||
<p class="featured-event-description"><%= event.description %></p>
|
||||
<div class="featured-event-footer">
|
||||
<span class="featured-event-price">€<%= event.ticket_types.minimum(:price_cents).to_f / 100 %></span>
|
||||
<%= link_to "Réserver une place", event_path(event.slug, event), class: "btn btn-sm btn-primary" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Price Badge -->
|
||||
<% if event.ticket_types.any? %>
|
||||
<div class="absolute bottom-4 right-4">
|
||||
<span class="bg-white/90 backdrop-blur-sm text-gray-900 px-3 py-1 rounded-full text-sm font-bold shadow-lg">
|
||||
À partir de €<%= event.ticket_types.minimum(:price_cents).to_f / 100 %>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: var(--space-12);">
|
||||
<%= link_to "Voir tous les événements", events_path, class: "btn btn-lg btn-outline" %>
|
||||
<!-- Event Info -->
|
||||
<div class="p-6 text-center">
|
||||
<h3 class="text-2xl lg:text-3xl font-bold text-gray-900 mb-2 group-hover:text-purple-600 transition-colors">
|
||||
<%= event.name.upcase %>
|
||||
</h3>
|
||||
|
||||
<div class="text-gray-600 space-y-1 mb-4">
|
||||
<div class="flex items-center justify-center text-sm">
|
||||
<i data-lucide="calendar" class="w-4 h-4 mr-2"></i>
|
||||
<%= l(event.start_time, format: '%A %d %B • %H:%M') %>
|
||||
</div>
|
||||
<div class="flex items-center justify-center text-sm">
|
||||
<i data-lucide="map-pin" class="w-4 h-4 mr-2"></i>
|
||||
<%= event.venue_name %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
<!-- Event Description -->
|
||||
<p class="text-gray-500 text-sm leading-relaxed max-w-sm mx-auto">
|
||||
<%= truncate(event.description, length: 100) %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section class="section features-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Pourquoi choisir Aperonight ?</h2>
|
||||
<p class="section-description">Nous sélectionnons des expériences premium qui connectent les professionnels et créent des relations durables.</p>
|
||||
<!-- More Events CTA -->
|
||||
<div class="text-center">
|
||||
<%= link_to events_path,
|
||||
class: "inline-flex items-center bg-gray-900 text-white px-8 py-4 rounded-full font-semibold text-lg hover:bg-gray-800 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" do %>
|
||||
Plus d'événements à Paris
|
||||
<i data-lucide="arrow-right" class="w-5 h-5 ml-2"></i>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i data-lucide="crown"></i>
|
||||
</div>
|
||||
<h3 class="feature-title">Sélection Premium</h3>
|
||||
<p class="feature-description">Chaque événement est soigneusement sélectionné et conçu pour offrir une valeur exceptionnelle et des opportunités de réseautage.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i data-lucide="shield-check"></i>
|
||||
</div>
|
||||
<h3 class="feature-title">Sécurisé et Fiable</h3>
|
||||
<p class="feature-description">Paiements sécurisés, lieux vérifiés et communauté de confiance avec couverture d'assurance complète.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i data-lucide="users-2"></i>
|
||||
</div>
|
||||
<h3 class="feature-title">Réseautage de Qualité</h3>
|
||||
<p class="feature-description">Connectez-vous avec des professionnels vérifiés, des entrepreneurs et des leaders de l'industrie dans des environnements intimes.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i data-lucide="zap"></i>
|
||||
</div>
|
||||
<h3 class="feature-title">Réservation Instantanée</h3>
|
||||
<p class="feature-description">Processus de réservation fluide avec confirmation instantanée et gestion d'événement facile.</p>
|
||||
<% else %>
|
||||
<!-- Empty State -->
|
||||
<div class="text-center py-16">
|
||||
<div class="w-24 h-24 bg-gradient-to-br from-purple-100 to-blue-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<i data-lucide="calendar-x" class="w-12 h-12 text-purple-600"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">Aucun événement disponible</h3>
|
||||
<p class="text-gray-600 mb-8 max-w-md mx-auto">Les événements arrivent bientôt. Inscrivez-vous pour être notifié des prochaines sorties!</p>
|
||||
<%= link_to new_user_registration_path, class: "bg-purple-600 text-white px-8 py-4 rounded-full font-semibold text-lg hover:bg-purple-700 transition-all duration-200 inline-flex items-center justify-center shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" do %>
|
||||
<i data-lucide="bell" class="w-5 h-5 mr-2"></i>
|
||||
Être notifié
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 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">Événements Mensuels</div>
|
||||
<!-- Site Metrics Section -->
|
||||
<section class="py-16 bg-gray-50">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl font-bold text-gray-900 mb-4">
|
||||
LA PLATEFORME DE RÉFÉRENCE
|
||||
</h2>
|
||||
<p class="text-xl text-gray-600">
|
||||
Rejoignez des milliers de professionnels qui font confiance à Aperonight
|
||||
</p>
|
||||
</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">Membres Actifs</div>
|
||||
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-8 text-center">
|
||||
<div class="group transform transition-all duration-300 hover:scale-105">
|
||||
<div class="text-4xl lg:text-5xl font-bold text-purple-600 mb-2 transition-colors group-hover:text-purple-700">
|
||||
<%= @total_events %>+
|
||||
</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">Lieux Partenaires</div>
|
||||
<div class="text-gray-600 font-medium">Événements organisés</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">Taux de Satisfaction</div>
|
||||
|
||||
<div class="group transform transition-all duration-300 hover:scale-105">
|
||||
<div class="text-4xl lg:text-5xl font-bold text-purple-600 mb-2 transition-colors group-hover:text-purple-700">
|
||||
<%= (@total_users / 100.0).round(1) %>k+
|
||||
</div>
|
||||
<div class="text-gray-600 font-medium">Membres actifs</div>
|
||||
</div>
|
||||
|
||||
<div class="group transform transition-all duration-300 hover:scale-105">
|
||||
<div class="text-4xl lg:text-5xl font-bold text-purple-600 mb-2 transition-colors group-hover:text-purple-700">
|
||||
<%= @events_this_month %>
|
||||
</div>
|
||||
<div class="text-gray-600 font-medium">Ce mois-ci</div>
|
||||
</div>
|
||||
|
||||
<div class="group transform transition-all duration-300 hover:scale-105">
|
||||
<div class="text-4xl lg:text-5xl font-bold text-purple-600 mb-2 transition-colors group-hover:text-purple-700">
|
||||
98%
|
||||
</div>
|
||||
<div class="text-gray-600 font-medium">Satisfaction</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="cta-section">
|
||||
<div class="container">
|
||||
<div class="cta-content">
|
||||
<h2>Prêt à rejoindre la communauté ?</h2>
|
||||
<p>Commencez à découvrir des événements incroyables et connectez-vous avec des professionnels partageant les mêmes idées dans votre ville.</p>
|
||||
<div style="display: flex; gap: var(--space-4); justify-content: center; flex-wrap: wrap;">
|
||||
<%= link_to new_user_registration_path, class: "btn btn-lg bg-white border-2 border-white text-blue-600 hover:bg-blue-400 hover:text-white" do %>
|
||||
<i data-lucide="user-plus"></i>
|
||||
Rejoindre gratuitement
|
||||
<section class="py-16 bg-gray-900 relative overflow-hidden">
|
||||
<!-- Background decoration -->
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900"></div>
|
||||
|
||||
<div class="relative max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h2 class="text-3xl lg:text-4xl font-bold text-white mb-6">
|
||||
Prêt à découvrir votre prochain événement ?
|
||||
</h2>
|
||||
<p class="text-xl text-gray-300 mb-8 max-w-2xl mx-auto">
|
||||
Rejoignez la communauté Aperonight et accédez aux meilleurs événements après-travail de Paris.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center max-w-lg mx-auto">
|
||||
<%= link_to events_path,
|
||||
class: "w-full sm:flex-1 bg-white text-gray-900 px-6 py-3 rounded-full font-semibold text-base hover:bg-gray-100 transition-all duration-200 inline-flex items-center justify-center shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" do %>
|
||||
<i data-lucide="search" class="w-4 h-4 mr-2"></i>
|
||||
Découvrir les événements
|
||||
<% end %>
|
||||
|
||||
<% unless user_signed_in? %>
|
||||
<%= link_to new_user_registration_path,
|
||||
class: "w-full sm:flex-1 border-2 border-white text-white px-6 py-3 rounded-full font-semibold text-base hover:bg-white hover:text-gray-900 transition-all duration-200 inline-flex items-center justify-center transform hover:-translate-y-0.5" do %>
|
||||
<i data-lucide="user-plus" class="w-4 h-4 mr-2"></i>
|
||||
Créer mon compte
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -116,28 +116,37 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :venue_address, "Adresse", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.text_field :venue_address, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "Ex: 1 Boulevard Poissonnière, 75002 Paris" %>
|
||||
</div>
|
||||
<%= form.label :venue_address, "Adresse complète", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<div class="space-y-2">
|
||||
<%= form.text_field :venue_address, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "Ex: 1 Boulevard Poissonnière, 75002 Paris", data: { "event-form-target": "address", action: "input->event-form#addressChanged" } %>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<%= form.label :latitude, "Latitude", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.number_field :latitude, step: :any, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "48.8566" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :longitude, "Longitude", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.number_field :longitude, step: :any, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "2.3522" %>
|
||||
<!-- Location Actions -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button type="button" data-action="click->event-form#getCurrentLocation" class="inline-flex items-center px-3 py-2 text-xs font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors">
|
||||
<i data-lucide="map-pin" class="w-3 h-3 mr-1"></i>
|
||||
Ma position
|
||||
</button>
|
||||
<button type="button" data-action="click->event-form#previewLocation" class="inline-flex items-center px-3 py-2 text-xs font-medium text-purple-700 bg-purple-50 border border-purple-200 rounded-lg hover:bg-purple-100 transition-colors">
|
||||
<i data-lucide="map" class="w-3 h-3 mr-1"></i>
|
||||
Prévisualiser
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-500">
|
||||
<p class="mt-2 text-sm text-gray-500">
|
||||
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
|
||||
Utilisez un service comme <a href="https://www.latlong.net/" target="_blank" class="text-purple-600 hover:text-purple-800">latlong.net</a> pour obtenir les coordonnées GPS.
|
||||
Les coordonnées GPS seront automatiquement calculées à partir de cette adresse.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Hidden coordinate fields for form submission -->
|
||||
<%= form.hidden_field :latitude, data: { "event-form-target": "latitude" } %>
|
||||
<%= form.hidden_field :longitude, data: { "event-form-target": "longitude" } %>
|
||||
|
||||
<!-- Map Links Container (shown when address is valid) -->
|
||||
<div data-event-form-target="mapLinksContainer" class="empty:hidden bg-gray-50 rounded-lg p-3 border border-gray-200"></div>
|
||||
</div>
|
||||
|
||||
<% if @event.published? && @event.tickets.any? %>
|
||||
<div class="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<div class="flex">
|
||||
@@ -154,11 +163,24 @@
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">Options</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center">
|
||||
<%= form.check_box :featured, class: "h-4 w-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500" %>
|
||||
<%= form.label :featured, "Mettre en avant sur la page d'accueil", class: "ml-2 text-sm text-gray-700" %>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500">Les événements mis en avant apparaissent en premier sur la page d'accueil.</p>
|
||||
<p class="text-sm text-gray-500">Les événements mis en avant apparaissent en premier sur la page d'accueil.</p>
|
||||
|
||||
<div class="flex items-start">
|
||||
<%= form.check_box :allow_booking_during_event, class: "h-4 w-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500 mt-1" %>
|
||||
<div class="ml-2">
|
||||
<%= form.label :allow_booking_during_event, "Autoriser la réservation pendant l'événement", class: "text-sm text-gray-700 font-medium" %>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Si activé, les participants pourront acheter des billets même après le début de l'événement.
|
||||
Si désactivé, la vente de billets s'arrêtera automatiquement à l'heure de début.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
<% content_for(:title, "Mes événements") %>
|
||||
|
||||
<div class="container py-8">
|
||||
<div class="min-h-screen max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<%= render 'components/breadcrumb', crumbs: [
|
||||
{ name: 'Accueil', path: root_path },
|
||||
{ name: 'Tableau de bord', path: dashboard_path },
|
||||
{ name: 'Mes événements' }
|
||||
] %>
|
||||
|
||||
<div class="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Mes événements</h1>
|
||||
<p class="text-gray-600">Gérez tous vos événements depuis cette interface</p>
|
||||
</div>
|
||||
<%= link_to new_promoter_event_path, class: "inline-flex items-center px-6 py-3 bg-black text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" do %>
|
||||
<%= link_to new_promoter_event_path, class: "inline-flex items-center px-6 py-3 bg-gray-900 text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" do %>
|
||||
<i data-lucide="plus" class="w-4 h-4 mr-2"></i>
|
||||
Créer un événement
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if @events.any? %>
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="bg-white rounded-2xl shadow-xl border border-gray-200 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50 border-b border-gray-200">
|
||||
@@ -121,13 +129,13 @@
|
||||
<%= paginate @events if respond_to?(:paginate) %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="bg-white rounded-lg border-2 border-dashed border-gray-300 p-12 text-center">
|
||||
<div class="bg-white rounded-2xl border-2 border-dashed border-gray-300 p-12 text-center">
|
||||
<div class="mx-auto h-24 w-24 rounded-full bg-gray-100 flex items-center justify-center mb-6">
|
||||
<i data-lucide="calendar-plus" class="w-12 h-12 text-gray-400"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-2">Aucun événement</h3>
|
||||
<p class="text-gray-500 mb-6">Vous n'avez pas encore créé d'événement. Commencez dès maintenant !</p>
|
||||
<%= link_to new_promoter_event_path, class: "inline-flex items-center px-6 py-3 bg-black text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" do %>
|
||||
<%= link_to new_promoter_event_path, class: "inline-flex items-center px-6 py-3 bg-gray-900 text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" do %>
|
||||
<i data-lucide="plus" class="w-4 h-4 mr-2"></i>
|
||||
Créer mon premier événement
|
||||
<% end %>
|
||||
|
||||
@@ -87,6 +87,9 @@
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">Lieu de l'événement</h3>
|
||||
|
||||
<!-- Geocoding Messages Container -->
|
||||
<div data-event-form-target="messagesContainer" class="space-y-3 mb-6 empty:mb-0 empty:hidden"></div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<%= form.label :venue_name, "Nom du lieu", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
@@ -94,27 +97,47 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :venue_address, "Adresse", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.text_field :venue_address, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "Ex: 1 Boulevard Poissonnière, 75002 Paris" %>
|
||||
</div>
|
||||
<%= form.label :venue_address, "Adresse complète", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<div class="space-y-2">
|
||||
<div class="relative">
|
||||
<%= form.text_field :venue_address, class: "w-full px-4 py-2 pr-12 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "Ex: 1 Boulevard Poissonnière, 75002 Paris", data: { "event-form-target": "address", action: "input->event-form#addressChanged" } %>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<%= form.label :latitude, "Latitude", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.number_field :latitude, step: :any, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "48.8566" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :longitude, "Longitude", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.number_field :longitude, step: :any, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "2.3522" %>
|
||||
<!-- Geocoding Loading Spinner -->
|
||||
<div data-event-form-target="geocodingSpinner" class="absolute right-3 top-1/2 transform -translate-y-1/2 hidden">
|
||||
<div class="w-5 h-5 border-2 border-purple-200 border-t-purple-600 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-500">
|
||||
<!-- Location Actions -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button type="button" data-action="click->event-form#getCurrentLocation" data-event-form-target="getCurrentLocationBtn" class="inline-flex items-center px-3 py-2 text-xs font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span data-event-form-target="getCurrentLocationIcon">
|
||||
<i data-lucide="map-pin" class="w-3 h-3 mr-1"></i>
|
||||
</span>
|
||||
<span data-event-form-target="getCurrentLocationText">Ma position</span>
|
||||
</button>
|
||||
<button type="button" data-action="click->event-form#previewLocation" data-event-form-target="previewLocationBtn" class="inline-flex items-center px-3 py-2 text-xs font-medium text-purple-700 bg-purple-50 border border-purple-200 rounded-lg hover:bg-purple-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span data-event-form-target="previewLocationIcon">
|
||||
<i data-lucide="map" class="w-3 h-3 mr-1"></i>
|
||||
</span>
|
||||
<span data-event-form-target="previewLocationText">Prévisualiser</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-sm text-gray-500">
|
||||
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
|
||||
Utilisez un service comme <a href="https://www.latlong.net/" target="_blank" class="text-purple-600 hover:text-purple-800">latlong.net</a> pour obtenir les coordonnées GPS.
|
||||
Les coordonnées GPS seront automatiquement calculées à partir de cette adresse.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Hidden coordinate fields populated by JavaScript geocoding -->
|
||||
<%= form.hidden_field :latitude, data: { "event-form-target": "latitude" } %>
|
||||
<%= form.hidden_field :longitude, data: { "event-form-target": "longitude" } %>
|
||||
|
||||
<!-- Map Links Container (shown when address is valid) -->
|
||||
<div data-event-form-target="mapLinksContainer" class="empty:hidden bg-gray-50 rounded-lg p-3 border border-gray-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<% content_for(:title, @event.name) %>
|
||||
|
||||
<div class="container py-8">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<%= render 'components/breadcrumb', crumbs: [
|
||||
{ name: 'Accueil', path: root_path },
|
||||
{ name: 'Tableau de bord', path: dashboard_path },
|
||||
{ name: 'Mes événements', path: promoter_events_path },
|
||||
{ name: @event.name }
|
||||
] %>
|
||||
|
||||
<!-- Header with actions -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -29,10 +38,17 @@
|
||||
<% end %>
|
||||
|
||||
<% if @event.draft? %>
|
||||
<% if @event.ticket_types.blank? %>
|
||||
<%= button_to publish_promoter_event_path(@event), method: :patch, disabled: true, class: "inline-flex items-center px-4 py-2 bg-gray-400 text-white font-medium rounded-lg cursor-not-allowed transition-colors duration-200", title: "Vous devez créer au moins un type de billet avant de publier" do %>
|
||||
<i data-lucide="upload" class="w-4 h-4 mr-2"></i>
|
||||
Publier
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= button_to publish_promoter_event_path(@event), method: :patch, class: "inline-flex items-center px-4 py-2 bg-green-600 text-white font-medium rounded-lg hover:bg-green-700 transition-colors duration-200" do %>
|
||||
<i data-lucide="upload" class="w-4 h-4 mr-2"></i>
|
||||
Publier
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% elsif @event.published? %>
|
||||
<%= button_to unpublish_promoter_event_path(@event), method: :patch, class: "inline-flex items-center px-4 py-2 bg-yellow-600 text-white font-medium rounded-lg hover:bg-yellow-700 transition-colors duration-200" do %>
|
||||
<i data-lucide="download" class="w-4 h-4 mr-2"></i>
|
||||
@@ -54,7 +70,7 @@
|
||||
<div class="mb-8">
|
||||
<% case @event.state %>
|
||||
<% when "draft" %>
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-2xl p-4">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="edit-3" class="w-5 h-5 text-gray-400 mr-3"></i>
|
||||
<div>
|
||||
@@ -63,8 +79,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if @event.ticket_types.blank? %>
|
||||
<div class="bg-amber-50 border border-amber-200 rounded-2xl p-4 mt-4">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="alert-triangle" class="w-5 h-5 text-amber-400 mr-3"></i>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-amber-900">Aucun type de billet configuré</h3>
|
||||
<p class="text-sm text-amber-700">Vous devez créer au moins un type de billet avant de pouvoir publier cet événement.</p>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<%= link_to promoter_event_ticket_types_path(@event), class: "text-amber-600 hover:text-amber-800 font-medium text-sm" do %>
|
||||
Configurer les billets <i data-lucide="external-link" class="w-4 h-4 inline ml-1"></i>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% when "published" %>
|
||||
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<div class="bg-green-50 border border-green-200 rounded-2xl p-4">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="eye" class="w-5 h-5 text-green-400 mr-3"></i>
|
||||
<div>
|
||||
@@ -79,7 +112,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<% when "canceled" %>
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<div class="bg-red-50 border border-red-200 rounded-2xl p-4">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="x-circle" class="w-5 h-5 text-red-400 mr-3"></i>
|
||||
<div>
|
||||
@@ -89,7 +122,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<% when "sold_out" %>
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-2xl p-4">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="users" class="w-5 h-5 text-blue-400 mr-3"></i>
|
||||
<div>
|
||||
@@ -101,7 +134,7 @@
|
||||
<% end %>
|
||||
|
||||
<% if @event.featured? %>
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mt-4">
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-2xl p-4 mt-4">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="star" class="w-5 h-5 text-yellow-400 mr-3"></i>
|
||||
<div>
|
||||
@@ -111,6 +144,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @event.published? && @event.event_started? && !@event.allow_booking_during_event? %>
|
||||
<div class="bg-orange-50 border border-orange-200 rounded-2xl p-4 mt-4">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="clock" class="w-5 h-5 text-orange-400 mr-3"></i>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-orange-900">Réservations fermées</h3>
|
||||
<p class="text-sm text-orange-700">L'événement a commencé et les nouvelles réservations sont désactivées.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Event details -->
|
||||
@@ -119,13 +164,13 @@
|
||||
<div class="lg:col-span-2 space-y-8">
|
||||
<!-- Event image -->
|
||||
<% if @event.image.present? %>
|
||||
<div class="aspect-video bg-gray-100 rounded-lg overflow-hidden">
|
||||
<div class="aspect-video bg-gray-100 rounded-2xl overflow-hidden">
|
||||
<img src="<%= @event.image %>" alt="<%= @event.name %>" class="w-full h-full object-cover">
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div class="bg-white rounded-2xl border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Description</h3>
|
||||
<div class="prose prose-gray max-w-none">
|
||||
<%= simple_format(@event.description) %>
|
||||
@@ -133,7 +178,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Location details -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div class="bg-white rounded-2xl border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Lieu</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-start space-x-3">
|
||||
@@ -154,7 +199,7 @@
|
||||
<!-- Sidebar -->
|
||||
<div class="space-y-6">
|
||||
<!-- Event stats -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div class="bg-white rounded-2xl border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Statistiques</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -175,13 +220,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Event info -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div class="bg-white rounded-2xl border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Informations</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">Slug</span>
|
||||
<p class="font-mono text-sm"><%= @event.slug %></p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">Créé le</span>
|
||||
<p class="text-sm"><%= @event.created_at.strftime("%d/%m/%Y à %H:%M") %></p>
|
||||
@@ -190,6 +231,18 @@
|
||||
<span class="text-sm text-gray-500">Modifié le</span>
|
||||
<p class="text-sm"><%= @event.updated_at.strftime("%d/%m/%Y à %H:%M") %></p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">Réservation pendant l'événement</span>
|
||||
<p class="text-sm flex items-center">
|
||||
<% if @event.allow_booking_during_event? %>
|
||||
<i data-lucide="check-circle" class="w-4 h-4 text-green-500 mr-1"></i>
|
||||
Autorisée
|
||||
<% else %>
|
||||
<i data-lucide="x-circle" class="w-4 h-4 text-red-500 mr-1"></i>
|
||||
Interdite
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
<% if @event.start_time %>
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">Début</span>
|
||||
@@ -206,7 +259,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Quick actions -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div class="bg-white rounded-2xl border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Actions rapides</h3>
|
||||
<div class="space-y-3">
|
||||
<%= link_to promoter_event_ticket_types_path(@event), class: "w-full inline-flex items-center px-4 py-2 bg-purple-600 text-white font-medium rounded-lg hover:bg-purple-700 transition-colors duration-200" do %>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<% content_for(:title, "Modifier #{@ticket_type.name}") %>
|
||||
|
||||
<div class="container py-8">
|
||||
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center space-x-4">
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
<% content_for(:title, "Types de billets - #{@event.name}") %>
|
||||
|
||||
<div class="container py-8">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<%= render 'components/breadcrumb', crumbs: [
|
||||
{ name: 'Accueil', path: root_path },
|
||||
{ name: 'Tableau de bord', path: dashboard_path },
|
||||
{ name: 'Mes événements', path: promoter_events_path },
|
||||
{ name: @event.name, path: promoter_event_path(@event) },
|
||||
{ name: 'Billets' }
|
||||
] %>
|
||||
|
||||
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center space-x-4 mb-4">
|
||||
<%= link_to promoter_event_path(@event), class: "text-gray-400 hover:text-gray-600 transition-colors" do %>
|
||||
@@ -12,7 +23,7 @@
|
||||
<%= link_to @event.name, promoter_event_path(@event), class: "text-purple-600 hover:text-purple-800" %>
|
||||
</p>
|
||||
</div>
|
||||
<%= link_to new_promoter_event_ticket_type_path(@event), class: "inline-flex items-center px-6 py-3 bg-black text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" do %>
|
||||
<%= link_to new_promoter_event_ticket_type_path(@event), class: "inline-flex items-center px-6 py-3 bg-gray-900 text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" do %>
|
||||
<i data-lucide="plus" class="w-4 h-4 mr-2"></i>
|
||||
Nouveau type
|
||||
<% end %>
|
||||
@@ -20,7 +31,7 @@
|
||||
|
||||
<!-- Event status info -->
|
||||
<% if @event.draft? %>
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-6">
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-2xl p-4 mb-6">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="info" class="w-5 h-5 text-gray-400 mr-3"></i>
|
||||
<p class="text-sm text-gray-600">
|
||||
@@ -34,7 +45,7 @@
|
||||
<% if @ticket_types.any? %>
|
||||
<div class="grid gap-6">
|
||||
<% @ticket_types.each do |ticket_type| %>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6 hover:shadow-md transition-shadow duration-200">
|
||||
<div class="bg-white rounded-2xl border border-gray-200 p-6 hover:shadow-md transition-shadow duration-200">
|
||||
<div class="flex items-start justify-between">
|
||||
<!-- Ticket type info -->
|
||||
<div class="flex-1">
|
||||
@@ -155,13 +166,13 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="bg-white rounded-lg border-2 border-dashed border-gray-300 p-12 text-center">
|
||||
<div class="bg-white rounded-2xl border-2 border-dashed border-gray-300 p-12 text-center">
|
||||
<div class="mx-auto h-24 w-24 rounded-full bg-gray-100 flex items-center justify-center mb-6">
|
||||
<i data-lucide="ticket" class="w-12 h-12 text-gray-400"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-2">Aucun type de billet</h3>
|
||||
<p class="text-gray-500 mb-6">Créez des types de billets pour permettre aux utilisateurs d'acheter des places pour votre événement.</p>
|
||||
<%= link_to new_promoter_event_ticket_type_path(@event), class: "inline-flex items-center px-6 py-3 bg-black text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" do %>
|
||||
<%= link_to new_promoter_event_ticket_type_path(@event), class: "inline-flex items-center px-6 py-3 bg-gray-900 text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" do %>
|
||||
<i data-lucide="plus" class="w-4 h-4 mr-2"></i>
|
||||
Créer mon premier type de billet
|
||||
<% end %>
|
||||
|
||||
123
app/views/settings/show.html.erb
Normal file
123
app/views/settings/show.html.erb
Normal file
@@ -0,0 +1,123 @@
|
||||
<div class="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-2xl mx-auto space-y-8">
|
||||
<!-- Breadcrumb -->
|
||||
<%= render 'components/breadcrumb', crumbs: [
|
||||
{ name: 'Accueil', path: root_path },
|
||||
{ name: 'Paramètres', path: settings_path }
|
||||
] %>
|
||||
</div>
|
||||
|
||||
<div class="max-w-2xl mx-auto space-y-8">
|
||||
<!-- Header -->
|
||||
<div class="text-center">
|
||||
<%= link_to "/" do %>
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-purple-600 to-blue-600 rounded-2xl mb-6 mx-auto">
|
||||
<i data-lucide="calendar" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<% end %>
|
||||
<h2 class="text-3xl font-bold text-gray-900">Paramètres du profil</h2>
|
||||
<p class="mt-2 text-gray-600">
|
||||
Gérez vos informations personnelles et préférences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Profile Form -->
|
||||
<div class="bg-white py-8 px-6 shadow-xl rounded-2xl">
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-6">Informations personnelles</h3>
|
||||
|
||||
<%= form_with model: @user, url: settings_path, method: :patch, local: true, html: { class: "space-y-6" } do |f| %>
|
||||
<% if @user.errors.any? %>
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<div class="flex">
|
||||
<i data-lucide="alert-circle" class="w-5 h-5 text-red-400 mt-0.5 mr-3"></i>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-red-800">Erreurs :</h3>
|
||||
<ul class="mt-2 text-sm text-red-700 list-disc list-inside">
|
||||
<% @user.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<%= f.label :first_name, "Prénom", class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i data-lucide="user" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
<%= f.text_field :first_name,
|
||||
class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors",
|
||||
placeholder: "Votre prénom" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :last_name, "Nom de famille", class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i data-lucide="user" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
<%= f.text_field :last_name,
|
||||
class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors",
|
||||
placeholder: "Votre nom de famille" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Professional Account Toggle -->
|
||||
<div class="border-t pt-6">
|
||||
<h4 class="text-lg font-medium text-gray-900 mb-4">Type de compte</h4>
|
||||
<div class="bg-gray-50 p-4 rounded-xl">
|
||||
<div class="flex items-start space-x-3">
|
||||
<%= f.check_box :is_professionnal,
|
||||
class: "mt-1 h-5 w-5 text-purple-600 border-gray-300 rounded focus:ring-purple-500" %>
|
||||
<div class="flex-1">
|
||||
<%= f.label :is_professionnal, "Compte professionnel",
|
||||
class: "block text-sm font-medium text-gray-900 cursor-pointer" %>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
Les comptes professionnels peuvent créer et gérer des événements.
|
||||
Cette option vous permet d'accéder aux fonctionnalités de promotion d'événements.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<%= f.button type: "submit", class: "group relative w-full flex justify-center items-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white bg-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" do %>
|
||||
<i data-lucide="save" class="w-4 h-4 mr-2"></i>
|
||||
Enregistrer les modifications
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<div class="bg-white py-6 px-6 shadow-xl rounded-2xl">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Gestion du compte</h3>
|
||||
<div class="space-y-3">
|
||||
<%= link_to edit_user_registration_path,
|
||||
class: "flex items-center p-3 text-gray-700 hover:bg-gray-50 rounded-lg transition-colors" do %>
|
||||
<i data-lucide="key" class="w-5 h-5 mr-3 text-gray-400"></i>
|
||||
<div>
|
||||
<div class="font-medium">Sécurité du compte</div>
|
||||
<div class="text-sm text-gray-500">Modifier l'email et le mot de passe</div>
|
||||
</div>
|
||||
<i data-lucide="chevron-right" class="w-5 h-5 ml-auto text-gray-400"></i>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Back Link -->
|
||||
<div class="text-center">
|
||||
<%= link_to :back, class: "inline-flex items-center text-purple-600 hover:text-purple-500 transition-colors" do %>
|
||||
<i data-lucide="arrow-left" class="w-4 h-4 mr-2"></i>
|
||||
Retour
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,7 +1,7 @@
|
||||
<% if flash.any? %>
|
||||
<div class="container">
|
||||
<div class="relative w-full flex justify-center p-4 mt-4">
|
||||
<div class="w-full max-w-xl">
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="relative w-full p-4 mt-4">
|
||||
<div class="w-full">
|
||||
<% flash.each do |type, message| %>
|
||||
<div class="notification <%= flash_class(type) %> flex items-center gap-3 p-4 rounded-lg mb-3 font-medium w-full box-border"
|
||||
data-controller="flash-message">
|
||||
|
||||
86
app/views/ticket_mailer/event_reminder.html.erb
Normal file
86
app/views/ticket_mailer/event_reminder.html.erb
Normal file
@@ -0,0 +1,86 @@
|
||||
<div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f8f9fa; border-radius: 8px;">
|
||||
<div style="text-align: center; padding: 20px 0; border-bottom: 1px solid #e9ecef;">
|
||||
<h1 style="color: #4c1d95; margin: 0; font-size: 28px;"><%= ENV.fetch("APP_NAME", "Aperonight") %></h1>
|
||||
<p style="color: #6c757d; margin: 10px 0 0;">Rappel d'événement</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: white; border-radius: 8px; padding: 30px; margin: 20px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<h2 style="color: #212529; margin-top: 0;">Salut <%= @user.email.split('@').first %> ! 🎉</h2>
|
||||
|
||||
<p style="color: #495057; line-height: 1.6; font-size: 18px;">
|
||||
<% case @days_before %>
|
||||
<% when 7 %>
|
||||
Plus qu'une semaine avant <strong><%= @event.name %></strong> !
|
||||
<% when 1 %>
|
||||
C'est demain ! <strong><%= @event.name %></strong> a lieu demain.
|
||||
<% when 0 %>
|
||||
C'est aujourd'hui ! <strong><%= @event.name %></strong> a lieu aujourd'hui.
|
||||
<% else %>
|
||||
Plus que <%= @days_before %> jours avant <strong><%= @event.name %></strong> !
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<div style="background-color: #f8f9fa; border-radius: 6px; padding: 20px; margin: 25px 0;">
|
||||
<h3 style="color: #4c1d95; margin-top: 0; border-bottom: 1px solid #e9ecef; padding-bottom: 10px;">Détails de l'événement</h3>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
|
||||
<div>
|
||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">📅 Date & heure</p>
|
||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529; font-size: 16px;"><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">📍 Lieu</p>
|
||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @event.venue_name %></p>
|
||||
<p style="margin: 5px 0 0; color: #495057;"><%= @event.venue_address %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #f8f9fa; border-radius: 6px; padding: 20px; margin: 25px 0;">
|
||||
<h4 style="color: #4c1d95; margin-top: 0; margin-bottom: 15px;">Vos billets pour cet événement :</h4>
|
||||
<% @tickets.each_with_index do |ticket, index| %>
|
||||
<div style="border: 1px solid #e9ecef; border-radius: 4px; padding: 15px; margin-bottom: 10px; background-color: white;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<p style="margin: 0 0 5px; font-weight: bold; color: #212529;">🎫 Billet #<%= index + 1 %></p>
|
||||
<p style="margin: 0; color: #6c757d; font-size: 14px;"><%= ticket.ticket_type.name %></p>
|
||||
<p style="margin: 5px 0 0;"><a href="<%= ticket_url(ticket) %>" style="color: #4c1d95; text-decoration: none; font-size: 14px;">📱 Voir le détail et le code QR</a></p>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<span style="background-color: #d4edda; color: #155724; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold;">ACTIF</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<% if @days_before == 0 %>
|
||||
<p style="color: #495057; margin-bottom: 20px; font-size: 16px;">🚨 N'oubliez pas vos billets ! Ils ont été envoyés par email lors de votre achat.</p>
|
||||
<% else %>
|
||||
<p style="color: #495057; margin-bottom: 20px;">📧 Vos billets ont été envoyés par email lors de votre achat.</p>
|
||||
<% end %>
|
||||
<p style="color: #495057; margin-bottom: 20px;">Présentez-les à l'entrée de l'événement pour y accéder.</p>
|
||||
</div>
|
||||
|
||||
<% if @days_before <= 1 %>
|
||||
<div style="background-color: #d1ecf1; border-radius: 6px; padding: 15px; border-left: 4px solid #17a2b8; margin: 20px 0;">
|
||||
<p style="margin: 0; color: #0c5460; font-size: 14px;">
|
||||
<strong>💡 Conseil :</strong> Arrivez un peu en avance pour éviter les files d'attente à l'entrée !
|
||||
</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<div style="background-color: #d4edda; border-radius: 6px; padding: 15px; border-left: 4px solid #28a745;">
|
||||
<p style="margin: 0; color: #155724; font-size: 14px;">
|
||||
<strong>📅 Ajoutez à votre calendrier :</strong> N'oubliez pas d'ajouter cet événement à votre calendrier pour ne pas le manquer !
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; color: #6c757d; font-size: 14px; padding: 20px 0;">
|
||||
<p style="margin: 0;">Des questions ? Contactez-nous à <a href="mailto:support@aperonight.com" style="color: #4c1d95; text-decoration: none;">support@aperonight.com</a></p>
|
||||
<p style="margin: 10px 0 0;">© <%= Time.current.year %> ApéroNight. Tous droits réservés.</p>
|
||||
</div>
|
||||
</div>
|
||||
41
app/views/ticket_mailer/event_reminder.text.erb
Normal file
41
app/views/ticket_mailer/event_reminder.text.erb
Normal file
@@ -0,0 +1,41 @@
|
||||
Salut <%= @user.email.split('@').first %> !
|
||||
|
||||
<% case @days_before %>
|
||||
<% when 7 %>
|
||||
Plus qu'une semaine avant "<%= @event.name %>" !
|
||||
<% when 1 %>
|
||||
C'est demain ! "<%= @event.name %>" a lieu demain.
|
||||
<% when 0 %>
|
||||
C'est aujourd'hui ! "<%= @event.name %>" a lieu aujourd'hui.
|
||||
<% else %>
|
||||
Plus que <%= @days_before %> jours avant "<%= @event.name %>" !
|
||||
<% end %>
|
||||
|
||||
DÉTAILS DE L'ÉVÉNEMENT
|
||||
======================
|
||||
|
||||
Date & heure : <%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||
Lieu : <%= @event.venue_name %>
|
||||
Adresse : <%= @event.venue_address %>
|
||||
|
||||
VOS BILLETS POUR CET ÉVÉNEMENT :
|
||||
<% @tickets.each_with_index do |ticket, index| %>
|
||||
- Billet #<%= index + 1 %> : <%= ticket.ticket_type.name %> (ACTIF)
|
||||
<% end %>
|
||||
|
||||
<% if @days_before == 0 %>
|
||||
N'oubliez pas vos billets ! Ils ont été envoyés par email lors de votre achat.
|
||||
<% else %>
|
||||
Vos billets ont été envoyés par email lors de votre achat.
|
||||
<% end %>
|
||||
Présentez-les à l'entrée de l'événement pour y accéder.
|
||||
|
||||
<% if @days_before <= 1 %>
|
||||
Conseil : Arrivez un peu en avance pour éviter les files d'attente à l'entrée !
|
||||
<% else %>
|
||||
N'oubliez pas d'ajouter cet événement à votre calendrier pour ne pas le manquer !
|
||||
<% end %>
|
||||
|
||||
Des questions ? Contactez-nous à support@aperonight.com
|
||||
|
||||
© <%= Time.current.year %> ApéroNight. Tous droits réservés.
|
||||
@@ -1,17 +1,68 @@
|
||||
<div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f8f9fa; border-radius: 8px;">
|
||||
<div style="text-align: center; padding: 20px 0; border-bottom: 1px solid #e9ecef;">
|
||||
<h1 style="color: #4c1d95; margin: 0; font-size: 28px;">ApéroNight</h1>
|
||||
<h1 style="color: #4c1d95; margin: 0; font-size: 28px;"><%= ENV.fetch("APP_NAME", "Aperonight") %></h1>
|
||||
<p style="color: #6c757d; margin: 10px 0 0;">Confirmation de votre achat</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: white; border-radius: 8px; padding: 30px; margin: 20px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<% if @user.first_name %>
|
||||
<h2 style="color: #212529; margin-top: 0;">Bonjour <%= @user.first_name %>,</h2>
|
||||
<% else %>
|
||||
<h2 style="color: #212529; margin-top: 0;">Bonjour <%= @user.email.split('@').first %>,</h2>
|
||||
<% end %>
|
||||
|
||||
<p style="color: #495057; line-height: 1.6;">
|
||||
<% if defined?(@order) && @order.present? %>
|
||||
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre commande pour l'événement <strong><%= @event.name %></strong>.
|
||||
<% else %>
|
||||
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre billet pour l'événement <strong><%= @event.name %></strong>.
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<div style="background-color: #f8f9fa; border-radius: 6px; padding: 20px; margin: 25px 0;">
|
||||
<% if defined?(@order) && @order.present? %>
|
||||
<h3 style="color: #4c1d95; margin-top: 0; border-bottom: 1px solid #e9ecef; padding-bottom: 10px;">Détails de votre commande</h3>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
|
||||
<div>
|
||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">Événement</p>
|
||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @event.name %></p>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">Date & heure</p>
|
||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<div>
|
||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">Nombre de billets</p>
|
||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @tickets.count %></p>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">Total</p>
|
||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= number_to_currency(@order.total_amount_euros, unit: "€") %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style="color: #4c1d95; margin: 20px 0 15px;">Billets inclus :</h4>
|
||||
<% @tickets.each_with_index do |ticket, index| %>
|
||||
<div style="border: 1px solid #e9ecef; border-radius: 4px; padding: 15px; margin-bottom: 10px; background-color: white;">
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<div>
|
||||
<p style="margin: 0 0 5px; font-weight: bold; color: #212529;">Billet #<%= index + 1 %></p>
|
||||
<p style="margin: 0; color: #6c757d; font-size: 14px;"><%= ticket.ticket_type.name %></p>
|
||||
<p style="margin: 5px 0 0;"><a href="<%= ticket_url(ticket) %>" style="color: #4c1d95; text-decoration: none; font-size: 14px;">📱 Voir le détail et le code QR</a></p>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<p style="margin: 0; font-weight: bold; color: #212529;"><%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<h3 style="color: #4c1d95; margin-top: 0; border-bottom: 1px solid #e9ecef; padding-bottom: 10px;">Détails de votre billet</h3>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
|
||||
@@ -35,22 +86,37 @@
|
||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px; text-align: center;">
|
||||
<a href="<%= ticket_url(@ticket) %>" style="color: #4c1d95; text-decoration: none; font-size: 14px; display: inline-block; padding: 10px 15px; border: 1px solid #4c1d95; border-radius: 6px; background-color: #f8f9fa;">📱 Voir le détail et le code QR</a>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<% if defined?(@order) && @order.present? %>
|
||||
<p style="color: #495057; margin-bottom: 20px;">Vos billets sont attachés à cet email en format PDF.</p>
|
||||
<p style="color: #495057; margin-bottom: 20px;">Présentez-les à l'entrée de l'événement pour y accéder.</p>
|
||||
<% else %>
|
||||
<p style="color: #495057; margin-bottom: 20px;">Votre billet est attaché à cet email en format PDF.</p>
|
||||
<p style="color: #495057; margin-bottom: 20px;">Présentez-le à l'entrée de l'événement pour y accéder.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #fff3cd; border-radius: 6px; padding: 15px; border-left: 4px solid #ffc107;">
|
||||
<p style="margin: 0; color: #856404; font-size: 14px;">
|
||||
<strong>Important :</strong> Ce billet est valable pour une seule entrée. Conservez-le précieusement.
|
||||
<strong>Important :</strong>
|
||||
<% if defined?(@order) && @order.present? %>
|
||||
Ces billets sont valables pour une seule entrée chacun. Conservez-les précieusement.
|
||||
<% else %>
|
||||
Ce billet est valable pour une seule entrée. Conservez-le précieusement.
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; color: #6c757d; font-size: 14px; padding: 20px 0;">
|
||||
<p style="margin: 0;">Si vous avez des questions, contactez-nous à <a href="mailto:support@aperonight.com" style="color: #4c1d95; text-decoration: none;">support@aperonight.com</a></p>
|
||||
<p style="margin: 10px 0 0;">© <%= Time.current.year %> ApéroNight. Tous droits réservés.</p>
|
||||
<p style="margin: 10px 0 0;">© <%= Time.current.year %> <%= Rails.application.config.app_name %>. Tous droits réservés.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +1,29 @@
|
||||
Bonjour <%= @user.email.split('@').first %>,
|
||||
<% if @user.first_name %>
|
||||
Bonjour <%= @user.first_name %>,
|
||||
<% else %>
|
||||
Bonjour <%= @user.email.split('@').first %>,
|
||||
<% end %>
|
||||
|
||||
<% if defined?(@order) && @order.present? %>
|
||||
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre commande pour l'événement "<%= @event.name %>".
|
||||
|
||||
DÉTAILS DE VOTRE COMMANDE
|
||||
=========================
|
||||
|
||||
Événement : <%= @event.name %>
|
||||
Date & heure : <%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||
Nombre de billets : <%= @tickets.count %>
|
||||
Total : <%= number_to_currency(@order.total_amount_euros, unit: "€") %>
|
||||
|
||||
BILLETS INCLUS :
|
||||
<% @tickets.each_with_index do |ticket, index| %>
|
||||
- Billet #<%= index + 1 %> : <%= ticket.ticket_type.name %> - <%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %>
|
||||
<% end %>
|
||||
|
||||
Vos billets sont attachés à cet email en format PDF. Présentez-les à l'entrée de l'événement pour y accéder.
|
||||
|
||||
Important : Ces billets sont valables pour une seule entrée chacun. Conservez-les précieusement.
|
||||
<% else %>
|
||||
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre billet pour l'événement "<%= @event.name %>".
|
||||
|
||||
DÉTAILS DE VOTRE BILLET
|
||||
@@ -13,7 +37,8 @@ Prix : <%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %>
|
||||
Votre billet est attaché à cet email en format PDF. Présentez-le à l'entrée de l'événement pour y accéder.
|
||||
|
||||
Important : Ce billet est valable pour une seule entrée. Conservez-le précieusement.
|
||||
<% end %>
|
||||
|
||||
Si vous avez des questions, contactez-nous à support@aperonight.com
|
||||
|
||||
© <%= Time.current.year %> ApéroNight. Tous droits réservés.
|
||||
© <%= Time.current.year %> <%= Rails.application.config.app_name %>. Tous droits réservés.
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Ticket #<%= ticket.id %></title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
color: #000000;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.ticket-container {
|
||||
max-width: 350px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #2D1B69;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.event-name {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.event-name h2 {
|
||||
color: #000000;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ticket-info {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.qr-code-section {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.qr-code-container svg {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ticket-container">
|
||||
<div class="header">
|
||||
<h1>ApéroNight</h1>
|
||||
</div>
|
||||
|
||||
<div class="event-name">
|
||||
<h2><%= ticket.event.name %></h2>
|
||||
</div>
|
||||
|
||||
<div class="ticket-info">
|
||||
<div class="info-row">
|
||||
<strong>Ticket Holder:</strong> <%= ticket.first_name %> <%= ticket.last_name %>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<strong>Ticket Type:</strong> <%= ticket.ticket_type.name %>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<strong>Price:</strong> €<%= ticket.price_euros %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="qr-code-section">
|
||||
<div class="qr-code-container">
|
||||
<%= raw ticket.generate_qr_svg %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -206,7 +206,7 @@
|
||||
</script>
|
||||
|
||||
<button onclick="redirectToCheckout(this)"
|
||||
class="w-full bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-medium py-4 px-6 rounded-xl shadow-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transform hover:-translate-y-0.5">
|
||||
class="w-full btn btn-primary py-4 px-6 rounded-xl shadow-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transform hover:-translate-y-0.5">
|
||||
<span class="flex items-center justify-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
"px-6 py-3 border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 text-center font-medium transition-colors duration-200" %>
|
||||
<%= form.submit "Procéder au paiement",
|
||||
class:
|
||||
"flex-1 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-medium py-3 px-6 rounded-xl shadow-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transform hover:-translate-y-0.5" %>
|
||||
"flex-1 btn btn-primary py-3 px-6 rounded-xl shadow-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transform hover:-translate-y-0.5" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -84,8 +84,8 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<%= link_to download_ticket_path(ticket, format: :pdf),
|
||||
class: "inline-flex items-center px-4 py-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white rounded-lg hover:from-purple-700 hover:to-indigo-700 transition-all duration-200 text-sm font-medium shadow-sm" do %>
|
||||
<%= link_to ticket_download_path(ticket.qr_code, format: :pdf),
|
||||
class: "inline-flex items-center px-4 py-2 btn btn-primary rounded-lg transition-all duration-200 text-sm font-medium shadow-sm" do %>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
@@ -129,7 +129,7 @@
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<%= link_to dashboard_path,
|
||||
class: "inline-flex items-center justify-center px-6 py-3 bg-gradient-to-r from-purple-600 to-indigo-600 text-white rounded-xl hover:from-purple-700 hover:to-indigo-700 transition-all duration-200 font-medium shadow-sm" do %>
|
||||
class: "inline-flex items-center justify-center px-6 py-3 btn btn-primary rounded-xl transition-all duration-200 font-medium shadow-sm" do %>
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
||||
</svg>
|
||||
|
||||
@@ -1,44 +1,30 @@
|
||||
<div class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 py-8">
|
||||
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 py-8">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="mb-8" aria-label="Breadcrumb">
|
||||
<ol class="flex items-center space-x-2 text-sm">
|
||||
<%= link_to root_path, class: "text-slate-500 hover:text-purple-600 transition-colors duration-200" do %>
|
||||
<svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
</svg>
|
||||
Accueil
|
||||
<% end %>
|
||||
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<%= link_to dashboard_path, class: "text-slate-500 hover:text-purple-600 transition-colors duration-200" do %>
|
||||
Tableau de bord
|
||||
<% end %>
|
||||
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<li class="font-medium text-slate-900" aria-current="page">Billet #<%= @ticket.id %></li>
|
||||
</ol>
|
||||
</nav>
|
||||
<%= render 'components/breadcrumb', crumbs: [
|
||||
{ name: 'Accueil', path: root_path },
|
||||
{ name: 'Tableau de bord', path: dashboard_path },
|
||||
{ name: "Commande ##{@order.id}", path: order_path(@order) },
|
||||
{ name: "Billet ##{@ticket.id}", path: nil }
|
||||
] %>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden border border-slate-200">
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<!-- Ticket Header -->
|
||||
<div class="bg-gradient-to-r from-purple-600 to-violet-600 px-8 py-6">
|
||||
<div class="bg-gradient-to-r from-purple-600 to-indigo-600 px-8 py-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl md:text-3xl font-bold text-white mb-2">Billet Électronique</h1>
|
||||
<h1 class="text-2xl md:text-3xl font-bold text-white mb-2">Billet électronique</h1>
|
||||
<p class="text-purple-100">ID: #<%= @ticket.id %></p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium <%=
|
||||
case @ticket.status
|
||||
when 'active' then 'bg-emerald-100 text-emerald-800'
|
||||
when 'draft' then 'bg-amber-100 text-amber-800'
|
||||
when 'used' then 'bg-slate-100 text-slate-800'
|
||||
when 'active' then 'bg-green-100 text-green-800'
|
||||
when 'draft' then 'bg-yellow-100 text-yellow-800'
|
||||
when 'used' then 'bg-gray-100 text-gray-800'
|
||||
when 'expired' then 'bg-red-100 text-red-800'
|
||||
when 'refunded' then 'bg-sky-100 text-sky-800'
|
||||
else 'bg-slate-100 text-slate-800'
|
||||
when 'refunded' then 'bg-blue-100 text-blue-800'
|
||||
else 'bg-gray-100 text-gray-800'
|
||||
end %>">
|
||||
<%=
|
||||
case @ticket.status
|
||||
@@ -58,49 +44,42 @@
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Event Details -->
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-900 mb-6">Détails de l'événement</h2>
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-6">Détails de l'événement</h2>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-500 mb-2">Événement</label>
|
||||
<p class="text-lg font-semibold text-slate-900"><%= @event.name %></p>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Événement</label>
|
||||
<p class="text-lg font-semibold text-gray-900"><%= @event.name %></p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-500 mb-2">Date et heure</label>
|
||||
<div class="flex items-start text-slate-900">
|
||||
<svg class="w-4 h-4 mr-2 mt-0.5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" 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"/>
|
||||
</svg>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Date et heure</label>
|
||||
<div class="flex items-center text-gray-900">
|
||||
<i data-lucide="calendar" class="w-4 h-4 mr-2 text-gray-400"></i>
|
||||
<%= @event.start_time.strftime("%d %B %Y") %><br>
|
||||
<small class="text-gray-600"><%= @event.start_time.strftime("%H:%M") %></small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium"><%= @event.start_time.strftime("%d %B %Y") %></div>
|
||||
<div class="text-sm text-slate-600"><%= @event.start_time.strftime("%H:%M") %></div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Lieu</label>
|
||||
<div class="flex items-center text-gray-900">
|
||||
<i data-lucide="map-pin" class="w-4 h-4 mr-2 text-gray-400"></i>
|
||||
<%= @event.venue_name %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-500 mb-2">Lieu</label>
|
||||
<div class="flex items-center text-slate-900">
|
||||
<svg class="w-4 h-4 mr-2 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
<span class="font-medium"><%= @event.venue_name %></span>
|
||||
</div>
|
||||
</div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Type de billet</label>
|
||||
<p class="text-gray-900 font-medium"><%= @ticket.ticket_type.name %></p>
|
||||
<p class="text-sm text-gray-600"><%= @ticket.ticket_type.description %></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-500 mb-2">Type de billet</label>
|
||||
<p class="text-slate-900 font-medium mb-1"><%= @ticket.ticket_type.name %></p>
|
||||
<p class="text-sm text-slate-600"><%= @ticket.ticket_type.description %></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-500 mb-2">Prix</label>
|
||||
<p class="text-2xl font-bold text-slate-900">
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Prix</label>
|
||||
<p class="text-xl font-bold text-gray-900">
|
||||
<%= number_to_currency(@ticket.price_euros, unit: "€", separator: ",", delimiter: " ", format: "%n %u") %>
|
||||
</p>
|
||||
</div>
|
||||
@@ -109,36 +88,40 @@
|
||||
|
||||
<!-- Ticket Details -->
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-900 mb-6">Informations du billet</h2>
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-6">Informations du billet</h2>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-500 mb-2">Prénom</label>
|
||||
<p class="text-slate-900 font-medium"><%= @ticket.first_name %></p>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Prénom</label>
|
||||
<p class="text-gray-900 font-medium"><%= @ticket.first_name %></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-500 mb-2">Nom</label>
|
||||
<p class="text-slate-900 font-medium"><%= @ticket.last_name %></p>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Nom</label>
|
||||
<p class="text-gray-900 font-medium"><%= @ticket.last_name %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-500 mb-2">Date d'achat</label>
|
||||
<p class="text-slate-900 font-medium"><%= @ticket.created_at.strftime("%d %B %Y à %H:%M") %></p>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Date d'achat</label>
|
||||
<p class="text-gray-900"><%= @ticket.created_at.strftime("%d %B %Y à %H:%M") %></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-500 mb-2">QR Code</label>
|
||||
<div class="bg-slate-50 rounded-xl p-6 text-center border border-slate-200">
|
||||
<div class="inline-block bg-white p-4 rounded-xl shadow-sm border border-slate-200">
|
||||
<div class="w-64 h-64 flex items-center justify-center">
|
||||
<%= raw @ticket.generate_qr_svg %>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">QR Code</label>
|
||||
<div class="bg-gray-50 rounded-lg p-4 text-center">
|
||||
<div class="inline-block bg-white p-4 rounded-lg shadow-sm">
|
||||
<div data-controller="qr-code" data-qr-code-data-value="<%= @ticket.qr_code %>" class="w-32 h-32">
|
||||
<!-- Loading indicator -->
|
||||
<div data-qr-code-target="loading" class="w-32 h-32 bg-gray-100 rounded flex items-center justify-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
|
||||
</div>
|
||||
<!-- QR code container -->
|
||||
<div data-qr-code-target="container" class="w-32 h-32"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 mt-3 font-mono tracking-wider"><%= @ticket.qr_code[0..7]... %></p>
|
||||
<p class="text-xs text-slate-400 mt-1">Scannez ce code à l'entrée</p>
|
||||
<p class="text-xs text-gray-500 mt-2 font-mono"><%= @ticket.qr_code %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,22 +129,18 @@
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="mt-8 pt-6 border-t border-slate-200">
|
||||
<div class="mt-8 pt-6 border-t border-gray-200">
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<%= link_to dashboard_path,
|
||||
class: "flex items-center justify-center px-6 py-3 border border-slate-300 text-slate-700 rounded-xl hover:bg-slate-50 hover:border-slate-400 font-medium transition-all duration-200" do %>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7 16l-4-4m0 0l4-4m-4 4h18"/>
|
||||
</svg>
|
||||
Retour au tableau de bord
|
||||
<%= link_to order_path(@order),
|
||||
class: "px-6 py-3 border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 text-center font-medium transition-colors duration-200" do %>
|
||||
<i data-lucide="arrow-left" class="w-4 h-4 inline-block mr-2"></i>
|
||||
Retour aux informations de commande
|
||||
<% end %>
|
||||
|
||||
<% if @ticket.status == 'active' %>
|
||||
<%= link_to download_ticket_path(@ticket.id),
|
||||
class: "flex-1 flex items-center justify-center bg-gradient-to-r from-purple-600 to-violet-600 hover:from-purple-700 hover:to-violet-700 text-white font-medium py-3 px-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transform hover:-translate-y-0.5" do %>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
<%= link_to ticket_download_path(@ticket.qr_code),
|
||||
class: "flex-1 btn btn-primary py-3 px-6 rounded-xl shadow-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transform hover:-translate-y-0.5 text-center" do %>
|
||||
<i data-lucide="download" class="w-4 h-4 inline-block mr-2"></i>
|
||||
Télécharger le PDF
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -169,26 +148,15 @@
|
||||
</div>
|
||||
|
||||
<!-- Important Notice -->
|
||||
<div class="mt-6 bg-sky-50 border border-sky-200 rounded-xl p-6">
|
||||
<div class="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-sky-600 mr-3 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<i data-lucide="info" class="w-5 h-5 text-blue-600 mr-2 mt-0.5"></i>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sky-800 font-semibold mb-2">Informations importantes</h3>
|
||||
<ul class="text-sky-700 text-sm space-y-2">
|
||||
<li class="flex items-start">
|
||||
<span class="w-1.5 h-1.5 bg-sky-600 rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
||||
Présentez ce billet (ou son code QR) à l'entrée de l'événement
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="w-1.5 h-1.5 bg-sky-600 rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
||||
Arrivez en avance pour éviter les files d'attente
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="w-1.5 h-1.5 bg-sky-600 rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
||||
En cas de problème, contactez l'organisateur
|
||||
</li>
|
||||
<h3 class="text-blue-800 font-medium mb-1">Informations importantes</h3>
|
||||
<ul class="text-blue-700 text-sm space-y-1">
|
||||
<li>• Présentez ce billet (ou son code QR) à l'entrée de l'événement</li>
|
||||
<li>• Arrivez en avance pour éviter les files d'attente</li>
|
||||
<li>• En cas de problème, contactez l'organisateur</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<% content_for :title, "Ticket ##{ticket.id}" %>
|
||||
|
||||
<div style="font-family: Arial, sans-serif; max-width: 350px; margin: 20px auto; padding: 20px; border: 1px solid #ccc;">
|
||||
<div style="text-align: center;">
|
||||
<h1 style="color: #2D1B69;">ApéroNight</h1>
|
||||
</div>
|
||||
<h2><%= ticket.event.name %></h2>
|
||||
<p>Ticket Holder: <%= ticket.first_name %> <%= ticket.last_name %></p>
|
||||
<p>Ticket Type: <%= ticket.ticket_type.name %></p>
|
||||
<p>Price: €<%= ticket.price_euros %></p>
|
||||
<div style="text-align: center; margin-top: 20px;">
|
||||
<%= raw ticket.generate_qr_svg %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,118 +0,0 @@
|
||||
<% content_for :title, "Billet ##{@ticket.id} - #{@ticket.event.name}" %>
|
||||
|
||||
<div class="min-h-screen bg-slate-100 py-8">
|
||||
<div class="max-w-md mx-auto px-4">
|
||||
<!-- Ticket Card -->
|
||||
<div class="max-w-md bg-white rounded-xl shadow-2xl overflow-hidden mx-auto border border-slate-200">
|
||||
<!-- Header -->
|
||||
<div class="bg-gradient-to-r from-purple-700 to-violet-600 text-center py-6 px-6">
|
||||
<h1 class="text-2xl font-bold text-white mb-2">ApéroNight</h1>
|
||||
<div class="w-16 h-0.5 bg-purple-200 mx-auto rounded-full"></div>
|
||||
</div>
|
||||
|
||||
<!-- Event Name -->
|
||||
<div class="text-center py-4 px-6 bg-purple-50 border-b border-purple-100">
|
||||
<h2 class="text-xl font-bold text-slate-900 leading-tight"><%= @ticket.event.name %></h2>
|
||||
</div>
|
||||
|
||||
<!-- Ticket Information -->
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- Ticket Holder -->
|
||||
<div class="flex justify-between items-center py-2 border-b border-slate-100">
|
||||
<span class="text-sm font-medium text-slate-600">Porteur du billet:</span>
|
||||
<span class="text-sm font-semibold text-slate-900 text-right"><%= @ticket.first_name %> <%= @ticket.last_name %></span>
|
||||
</div>
|
||||
|
||||
<!-- Ticket Type -->
|
||||
<div class="flex justify-between items-center py-2 border-b border-slate-100">
|
||||
<span class="text-sm font-medium text-slate-600">Type de billet:</span>
|
||||
<span class="text-sm font-semibold text-slate-900"><%= @ticket.ticket_type.name %></span>
|
||||
</div>
|
||||
|
||||
<!-- Price -->
|
||||
<div class="flex justify-between items-center py-2 border-b border-slate-100">
|
||||
<span class="text-sm font-medium text-slate-600">Prix:</span>
|
||||
<span class="text-sm font-semibold text-slate-900">
|
||||
<%= number_to_currency(@ticket.price_euros, unit: "€", separator: ",", delimiter: " ", format: "%n %u") %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Date & Time -->
|
||||
<div class="flex justify-between items-center py-2 border-b border-slate-100">
|
||||
<span class="text-sm font-medium text-slate-600">Date & Heure:</span>
|
||||
<div class="text-right">
|
||||
<div class="text-sm font-semibold text-slate-900"><%= @ticket.event.start_time.strftime("%d %B %Y") %></div>
|
||||
<div class="text-xs text-slate-600"><%= @ticket.event.start_time.strftime("%H:%M") %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Venue -->
|
||||
<div class="py-2 border-b border-slate-100">
|
||||
<span class="text-sm font-medium text-slate-600 block mb-1">Lieu :</span>
|
||||
<div class="text-sm font-semibold text-slate-900"><%= @ticket.event.venue_name %></div>
|
||||
<% if @ticket.event.venue_address.present? %>
|
||||
<div class="text-xs text-slate-600 mt-1"><%= @ticket.event.venue_address %></div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Code Section -->
|
||||
<div class="bg-slate-50 p-6 text-center border-t border-slate-200">
|
||||
<h3 class="text-sm font-semibold text-slate-900 mb-4">Code QR du billet</h3>
|
||||
<div class="inline-block bg-white p-6 rounded-xl shadow-sm border border-slate-200">
|
||||
<div class="w-52 h-52 flex items-center justify-center qr-code-container">
|
||||
<%= raw @ticket.generate_qr_svg %>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 mt-3 font-mono tracking-wider">QR: <%= @ticket.qr_code[0..7] %>...</p>
|
||||
</div>
|
||||
|
||||
<!-- Footer Notice -->
|
||||
<div class="bg-slate-100 px-6 py-4 text-center border-t border-slate-200">
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs text-slate-600">Ce billet est valide pour une seule entrée.</p>
|
||||
<p class="text-xs text-slate-600">Présentez ce billet à l'entrée du lieu.</p>
|
||||
<div class="pt-2 border-t border-slate-200">
|
||||
<p class="text-xs text-slate-500">
|
||||
Généré le <%= Time.current.strftime('%d %B %Y à %H:%M') %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="p-4 bg-white border-t border-slate-200">
|
||||
<div class="flex space-x-2">
|
||||
<%= link_to ticket_path(@ticket),
|
||||
class: "flex-1 flex items-center justify-center bg-slate-100 hover:bg-slate-200 text-slate-700 py-2.5 px-3 rounded-lg text-sm font-medium transition-colors duration-200" do %>
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
Vue détaillée
|
||||
<% end %>
|
||||
|
||||
<% if @ticket.status == 'active' %>
|
||||
<%= link_to download_ticket_path(@ticket.id),
|
||||
class: "flex-1 flex items-center justify-center bg-purple-600 hover:bg-purple-700 text-white py-2.5 px-3 rounded-lg text-sm font-medium transition-colors duration-200 shadow-sm hover:shadow-md" do %>
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
PDF
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="text-center mt-6">
|
||||
<%= link_to dashboard_path, class: "inline-flex items-center text-purple-600 hover:text-purple-800 text-sm font-medium transition-colors duration-200" do %>
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7 16l-4-4m0 0l4-4m-4 4h18"/>
|
||||
</svg>
|
||||
Retour au tableau de bord
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user