Compare commits
78 Commits
2f80fe8321
...
feat/seo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5105964b39 | ||
|
|
fa99a167a5 | ||
|
|
9b33b73bb4 | ||
|
|
bc47027c22 | ||
|
|
7ef934d8a8 | ||
|
|
974edce238 | ||
|
|
7009245ab0 | ||
|
|
a984243fe2 | ||
|
|
01b545c83e | ||
|
|
cb0de11de1 | ||
|
|
1daeee0eb1 | ||
|
|
ff32b6f21c | ||
|
|
8544802b7f | ||
|
|
0abf8d9aa9 | ||
|
|
da420ccd76 | ||
|
|
24a4560634 | ||
|
|
ed5ff4b8fd | ||
|
|
ffd9d31c94 | ||
|
|
eee7855d36 | ||
|
|
ea7517457a | ||
|
|
6d3ee7e400 | ||
|
|
15e3c7dff5 | ||
|
|
46c8faf10c | ||
|
|
a3689948ae | ||
|
|
d18c1a7b3e | ||
|
|
a0e53325f7 | ||
|
|
61079c8171 | ||
|
|
e1edc1afcd | ||
|
|
bd6c0d5ed8 | ||
|
|
5fc790cd42 | ||
|
|
ec5095d372 | ||
|
|
31f5d2188d | ||
|
|
e866e259bb | ||
|
|
54e99c2f7e | ||
|
|
3ba5710d8f | ||
|
|
0f6d75b1e8 | ||
|
|
ee4399aa46 | ||
|
|
839120f2f4 | ||
|
|
6965eb89fd | ||
|
|
0ba6634e99 | ||
|
|
ca81d2360c | ||
|
|
afe074c8a1 | ||
|
|
e838e91162 | ||
|
|
aa5dccb508 | ||
|
|
3414057795 | ||
|
|
1acc3e09d4 | ||
|
|
48c648e2ca | ||
|
|
b493027c86 | ||
|
|
6ea3005a65 | ||
|
|
476438c5c4 | ||
|
|
055640b73e | ||
|
|
a7e83d79d7 | ||
|
|
9404f10c93 | ||
|
|
907e51fc60 | ||
|
|
56b0a45719 | ||
|
|
58dbcf3a6a | ||
|
|
394190ace8 | ||
|
|
2a2c249a54 | ||
|
|
3fa9249bc8 | ||
|
|
b9576b91f5 | ||
|
|
be3d80e541 | ||
|
|
0b58768a24 | ||
|
|
911e821948 | ||
|
|
2fd93dc3bf | ||
|
|
a3dce5c363 | ||
|
|
7cdb9f468c | ||
|
|
4e2445198f | ||
|
|
49ad935855 | ||
|
|
1989cbf6af | ||
|
|
784d5158b4 | ||
|
|
b2d1cb5fa4 | ||
|
|
74f8350abe | ||
|
|
96734480d5 | ||
|
|
f6675bd5e4 | ||
|
|
a8a8c55041 | ||
|
|
9513867614 | ||
|
|
30f3ecc6ad | ||
|
|
6f31f99def |
0
.cursor/rules/design.mdc
Normal file → Executable file
0
.cursor/rules/design.mdc
Normal file → Executable file
0
.dockerignore
Normal file → Executable file
0
.dockerignore
Normal file → Executable file
0
.editorconfig
Normal file → Executable file
0
.editorconfig
Normal file → Executable file
6
.env.example
Normal file → Executable file
6
.env.example
Normal file → Executable file
@@ -39,9 +39,11 @@ SMTP_ENABLE_STARTTLS=false
|
|||||||
# SMTP_STARTTLS=true
|
# SMTP_STARTTLS=true
|
||||||
|
|
||||||
# Application variables
|
# Application variables
|
||||||
STRIPE_API_KEY=1337
|
STRIPE_PUBLISHABLE_KEY=pk_test_51S1M7BJWx6G2LLIXYpTvi0hxMpZ4tZSxkmr2Wbp1dQ73MKNp4Tyu4xFJBqLXK5nn4E0nEf2tdgJqEwWZLosO3QGn00kMvjXWGW
|
||||||
|
STRIPE_SECRET_KEY=sk_test_51S1M7BJWx6G2LLIXK2pdLpRKb9Mgd3sZ30N4ueVjHepgxQKbWgMVJoa4v4ESzHQ6u6zJjO4jUvgLYPU1QLyAiFTN00sGz2ortW
|
||||||
|
STRIPE_WEBHOOK_SECRET=LaReunion974
|
||||||
|
|
||||||
# OpenAI login
|
# Scaleway login
|
||||||
OPENAI_API_KEY=f66dbb5f-9770-4f81-b2ea-eb7370bc9aa5
|
OPENAI_API_KEY=f66dbb5f-9770-4f81-b2ea-eb7370bc9aa5
|
||||||
OPENAI_BASE_URL=https://api.scaleway.ai/v1
|
OPENAI_BASE_URL=https://api.scaleway.ai/v1
|
||||||
OPENAI_MODEL=devstral-small-2505
|
OPENAI_MODEL=devstral-small-2505
|
||||||
|
|||||||
0
.gitattributes
vendored
Normal file → Executable file
0
.gitattributes
vendored
Normal file → Executable file
0
.github/dependabot.yml
vendored
Normal file → Executable file
0
.github/dependabot.yml
vendored
Normal file → Executable file
0
.github/workflows/ci.yml
vendored
Normal file → Executable file
0
.github/workflows/ci.yml
vendored
Normal file → Executable file
3
.gitignore
vendored
Normal file → Executable file
3
.gitignore
vendored
Normal file → Executable file
@@ -43,3 +43,6 @@
|
|||||||
|
|
||||||
# Ignore generated reports
|
# Ignore generated reports
|
||||||
/test/reports
|
/test/reports
|
||||||
|
|
||||||
|
# Ignore .fuse file
|
||||||
|
.fuse_hidden*
|
||||||
0
.kamal/hooks/docker-setup.sample
Normal file → Executable file
0
.kamal/hooks/docker-setup.sample
Normal file → Executable file
0
.kamal/hooks/post-app-boot.sample
Normal file → Executable file
0
.kamal/hooks/post-app-boot.sample
Normal file → Executable file
0
.kamal/hooks/post-deploy.sample
Normal file → Executable file
0
.kamal/hooks/post-deploy.sample
Normal file → Executable file
0
.kamal/hooks/post-proxy-reboot.sample
Normal file → Executable file
0
.kamal/hooks/post-proxy-reboot.sample
Normal file → Executable file
0
.kamal/hooks/pre-app-boot.sample
Normal file → Executable file
0
.kamal/hooks/pre-app-boot.sample
Normal file → Executable file
0
.kamal/hooks/pre-build.sample
Normal file → Executable file
0
.kamal/hooks/pre-build.sample
Normal file → Executable file
0
.kamal/hooks/pre-connect.sample
Normal file → Executable file
0
.kamal/hooks/pre-connect.sample
Normal file → Executable file
0
.kamal/hooks/pre-deploy.sample
Normal file → Executable file
0
.kamal/hooks/pre-deploy.sample
Normal file → Executable file
0
.kamal/hooks/pre-proxy-reboot.sample
Normal file → Executable file
0
.kamal/hooks/pre-proxy-reboot.sample
Normal file → Executable file
0
.kamal/secrets
Normal file → Executable file
0
.kamal/secrets
Normal file → Executable file
0
.node-version
Normal file → Executable file
0
.node-version
Normal file → Executable file
0
.rubocop.yml
Normal file → Executable file
0
.rubocop.yml
Normal file → Executable file
0
.ruby-version
Normal file → Executable file
0
.ruby-version
Normal file → Executable file
@@ -0,0 +1,738 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
: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;
|
||||||
|
}
|
||||||
710
.superdesign/design_iterations/aperonight_premium_login_1.html
Normal file
710
.superdesign/design_iterations/aperonight_premium_login_1.html
Normal file
@@ -0,0 +1,710 @@
|
|||||||
|
<!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>
|
||||||
125
.superdesign/design_iterations/aperonight_premium_theme.css
Normal file
125
.superdesign/design_iterations/aperonight_premium_theme.css
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
: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;
|
||||||
|
}
|
||||||
385
.superdesign/design_iterations/dashboard_2.html
Normal file
385
.superdesign/design_iterations/dashboard_2.html
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
<!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>
|
||||||
556
.superdesign/design_iterations/dashboard_3.html
Normal file
556
.superdesign/design_iterations/dashboard_3.html
Normal file
@@ -0,0 +1,556 @@
|
|||||||
|
<!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>
|
||||||
0
.superdesign/design_iterations/default_ui_darkmode.css
Normal file → Executable file
0
.superdesign/design_iterations/default_ui_darkmode.css
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,618 +0,0 @@
|
|||||||
/* Enhanced Aperonight Design System - Event Booking Optimized */
|
|
||||||
:root {
|
|
||||||
/* Enhanced Purple gradient system with more depth */
|
|
||||||
--color-primary-50: #faf5ff;
|
|
||||||
--color-primary-100: #f3e8ff;
|
|
||||||
--color-primary-200: #e9d5ff;
|
|
||||||
--color-primary-300: #d8b4fe;
|
|
||||||
--color-primary-400: #c084fc;
|
|
||||||
--color-primary-500: #a855f7;
|
|
||||||
--color-primary-600: #9333ea;
|
|
||||||
--color-primary-700: #7e22ce;
|
|
||||||
--color-primary-800: #6b21a8;
|
|
||||||
--color-primary-900: #581c87;
|
|
||||||
--color-primary-950: #3b0764; /* Added for deeper contrast */
|
|
||||||
|
|
||||||
/* Enhanced Pink gradient for event highlights */
|
|
||||||
--color-accent-300: #f9a8d4;
|
|
||||||
--color-accent-400: #f472b6;
|
|
||||||
--color-accent-500: #ec4899;
|
|
||||||
--color-accent-600: #db2777;
|
|
||||||
--color-accent-700: #be185d; /* Added for better hierarchy */
|
|
||||||
|
|
||||||
/* Enhanced Neutral system with warmer tones */
|
|
||||||
--color-neutral-50: #f8fafc;
|
|
||||||
--color-neutral-100: #f1f5f9;
|
|
||||||
--color-neutral-200: #e2e8f0;
|
|
||||||
--color-neutral-300: #cbd5e1;
|
|
||||||
--color-neutral-400: #94a3b8;
|
|
||||||
--color-neutral-500: #64748b;
|
|
||||||
--color-neutral-600: #475569;
|
|
||||||
--color-neutral-700: #334155;
|
|
||||||
--color-neutral-800: #1e293b;
|
|
||||||
--color-neutral-900: #0f172a;
|
|
||||||
--color-neutral-950: #020617; /* Added for deeper backgrounds */
|
|
||||||
|
|
||||||
/* Event-specific semantic colors */
|
|
||||||
--color-success-light: #dcfce7;
|
|
||||||
--color-success: #16a34a;
|
|
||||||
--color-success-dark: #15803d;
|
|
||||||
--color-warning-light: #fef3c7;
|
|
||||||
--color-warning: #f59e0b;
|
|
||||||
--color-warning-dark: #d97706;
|
|
||||||
--color-danger-light: #fee2e2;
|
|
||||||
--color-danger: #dc2626;
|
|
||||||
--color-danger-dark: #b91c1c;
|
|
||||||
|
|
||||||
/* Event status colors */
|
|
||||||
--color-event-featured: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-accent-500) 100%);
|
|
||||||
--color-event-available: var(--color-success);
|
|
||||||
--color-event-limited: var(--color-warning);
|
|
||||||
--color-event-sold-out: var(--color-danger);
|
|
||||||
--color-event-vip: linear-gradient(135deg, #ffd700 0%, #ffb347 100%);
|
|
||||||
|
|
||||||
/* Enhanced Typography with better hierarchy */
|
|
||||||
--font-sans: 'Plus Jakarta Sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
||||||
--font-display: 'Outfit', var(--font-sans); /* For headings and key content */
|
|
||||||
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, 'SF Mono', monospace;
|
|
||||||
|
|
||||||
/* Enhanced font sizes with golden ratio scaling */
|
|
||||||
--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 - for hero sections */
|
|
||||||
--text-6xl: 3.75rem; /* 60px - for major headings */
|
|
||||||
|
|
||||||
/* Enhanced spacing system */
|
|
||||||
--space-px: 1px;
|
|
||||||
--space-0-5: 0.125rem; /* 2px */
|
|
||||||
--space-1: 0.25rem; /* 4px */
|
|
||||||
--space-1-5: 0.375rem; /* 6px */
|
|
||||||
--space-2: 0.5rem; /* 8px */
|
|
||||||
--space-2-5: 0.625rem; /* 10px */
|
|
||||||
--space-3: 0.75rem; /* 12px */
|
|
||||||
--space-3-5: 0.875rem; /* 14px */
|
|
||||||
--space-4: 1rem; /* 16px */
|
|
||||||
--space-5: 1.25rem; /* 20px */
|
|
||||||
--space-6: 1.5rem; /* 24px */
|
|
||||||
--space-7: 1.75rem; /* 28px */
|
|
||||||
--space-8: 2rem; /* 32px */
|
|
||||||
--space-9: 2.25rem; /* 36px */
|
|
||||||
--space-10: 2.5rem; /* 40px */
|
|
||||||
--space-11: 2.75rem; /* 44px */
|
|
||||||
--space-12: 3rem; /* 48px */
|
|
||||||
--space-14: 3.5rem; /* 56px */
|
|
||||||
--space-16: 4rem; /* 64px */
|
|
||||||
--space-20: 5rem; /* 80px */
|
|
||||||
--space-24: 6rem; /* 96px */
|
|
||||||
|
|
||||||
/* Enhanced border radius system */
|
|
||||||
--radius-none: 0px;
|
|
||||||
--radius-sm: 0.25rem; /* 4px */
|
|
||||||
--radius-md: 0.375rem; /* 6px */
|
|
||||||
--radius: 0.5rem; /* 8px */
|
|
||||||
--radius-lg: 0.75rem; /* 12px */
|
|
||||||
--radius-xl: 1rem; /* 16px */
|
|
||||||
--radius-2xl: 1.5rem; /* 24px */
|
|
||||||
--radius-3xl: 2rem; /* 32px */
|
|
||||||
--radius-full: 9999px;
|
|
||||||
|
|
||||||
/* Enhanced shadow system with color variations */
|
|
||||||
--shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
||||||
--shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
||||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
||||||
--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);
|
|
||||||
--shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
|
|
||||||
|
|
||||||
/* Purple-tinted shadows for premium feel */
|
|
||||||
--shadow-purple-sm: 0 1px 3px 0 rgb(147 51 234 / 0.1), 0 1px 2px -1px rgb(147 51 234 / 0.1);
|
|
||||||
--shadow-purple-md: 0 4px 6px -1px rgb(147 51 234 / 0.1), 0 2px 4px -2px rgb(147 51 234 / 0.1);
|
|
||||||
--shadow-purple-lg: 0 10px 15px -3px rgb(147 51 234 / 0.15), 0 4px 6px -4px rgb(147 51 234 / 0.1);
|
|
||||||
|
|
||||||
/* Pink-tinted shadows for event highlights */
|
|
||||||
--shadow-pink-sm: 0 1px 3px 0 rgb(236 72 153 / 0.1), 0 1px 2px -1px rgb(236 72 153 / 0.1);
|
|
||||||
--shadow-pink-md: 0 4px 6px -1px rgb(236 72 153 / 0.1), 0 2px 4px -2px rgb(236 72 153 / 0.1);
|
|
||||||
|
|
||||||
/* Animation durations */
|
|
||||||
--duration-fast: 0.15s;
|
|
||||||
--duration-normal: 0.2s;
|
|
||||||
--duration-slow: 0.3s;
|
|
||||||
--duration-slower: 0.5s;
|
|
||||||
|
|
||||||
/* Easing functions */
|
|
||||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
|
||||||
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
|
||||||
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced Component Styles */
|
|
||||||
|
|
||||||
/* Buttons with improved hierarchy */
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-weight: 600;
|
|
||||||
white-space: nowrap;
|
|
||||||
transition: all var(--duration-normal) var(--ease-in-out);
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sm {
|
|
||||||
padding: var(--space-2) var(--space-3);
|
|
||||||
font-size: var(--text-sm);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-md {
|
|
||||||
padding: var(--space-3) var(--space-4);
|
|
||||||
font-size: var(--text-base);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-lg {
|
|
||||||
padding: var(--space-4) var(--space-6);
|
|
||||||
font-size: var(--text-lg);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-accent-500) 100%);
|
|
||||||
color: white;
|
|
||||||
box-shadow: var(--shadow-purple-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: linear-gradient(135deg, var(--color-primary-700) 0%, var(--color-accent-600) 100%);
|
|
||||||
box-shadow: var(--shadow-purple-md);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: var(--shadow-purple-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: white;
|
|
||||||
color: var(--color-primary-600);
|
|
||||||
border: 1px solid var(--color-neutral-200);
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: var(--color-primary-50);
|
|
||||||
border-color: var(--color-primary-300);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline {
|
|
||||||
background: transparent;
|
|
||||||
color: var(--color-primary-600);
|
|
||||||
border: 2px solid var(--color-primary-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline:hover {
|
|
||||||
background: var(--color-primary-600);
|
|
||||||
color: white;
|
|
||||||
box-shadow: var(--shadow-purple-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost {
|
|
||||||
background: transparent;
|
|
||||||
color: var(--color-neutral-600);
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost:hover {
|
|
||||||
background: var(--color-neutral-100);
|
|
||||||
color: var(--color-primary-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced Cards */
|
|
||||||
.card {
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-xl);
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
border: 1px solid var(--color-neutral-200);
|
|
||||||
overflow: hidden;
|
|
||||||
transition: all var(--duration-normal) var(--ease-in-out);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-interactive {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-interactive:hover {
|
|
||||||
box-shadow: var(--shadow-xl);
|
|
||||||
transform: translateY(-4px);
|
|
||||||
border-color: var(--color-primary-200);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
padding: var(--space-6);
|
|
||||||
border-bottom: 1px solid var(--color-neutral-200);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body {
|
|
||||||
padding: var(--space-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-footer {
|
|
||||||
padding: var(--space-4) var(--space-6);
|
|
||||||
background: var(--color-neutral-50);
|
|
||||||
border-top: 1px solid var(--color-neutral-200);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Event-specific cards */
|
|
||||||
.event-card {
|
|
||||||
position: relative;
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-xl);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
overflow: hidden;
|
|
||||||
transition: all var(--duration-slow) var(--ease-out);
|
|
||||||
border: 1px solid var(--color-neutral-200);
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-card:hover {
|
|
||||||
box-shadow: var(--shadow-2xl);
|
|
||||||
transform: translateY(-6px) scale(1.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-card-featured {
|
|
||||||
border: 2px solid transparent;
|
|
||||||
background: linear-gradient(white, white) padding-box,
|
|
||||||
linear-gradient(135deg, var(--color-primary-600), var(--color-accent-500)) border-box;
|
|
||||||
box-shadow: var(--shadow-purple-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-card-featured::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 4px;
|
|
||||||
background: var(--color-event-featured);
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-card-image {
|
|
||||||
aspect-ratio: 16/9;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-card-image img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
transition: transform var(--duration-slow) var(--ease-out);
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-card:hover .event-card-image img {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced Forms */
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: var(--space-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-label {
|
|
||||||
display: block;
|
|
||||||
font-size: var(--text-sm);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-neutral-700);
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
font-family: var(--font-display);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input,
|
|
||||||
.form-select,
|
|
||||||
.form-textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: var(--space-3) var(--space-4);
|
|
||||||
font-size: var(--text-base);
|
|
||||||
color: var(--color-neutral-900);
|
|
||||||
background: white;
|
|
||||||
border: 1px solid var(--color-neutral-300);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
transition: all var(--duration-normal) var(--ease-in-out);
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input:focus,
|
|
||||||
.form-select:focus,
|
|
||||||
.form-textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--color-primary-500);
|
|
||||||
box-shadow: 0 0 0 3px rgb(168 85 247 / 0.1);
|
|
||||||
background: var(--color-primary-50);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-error {
|
|
||||||
color: var(--color-danger);
|
|
||||||
font-size: var(--text-sm);
|
|
||||||
margin-top: var(--space-1);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced Badges */
|
|
||||||
.badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-1);
|
|
||||||
padding: var(--space-1-5) var(--space-3);
|
|
||||||
font-size: var(--text-xs);
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
font-family: var(--font-display);
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-available {
|
|
||||||
background: var(--color-success-light);
|
|
||||||
color: var(--color-success-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-limited {
|
|
||||||
background: var(--color-warning-light);
|
|
||||||
color: var(--color-warning-dark);
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-sold-out {
|
|
||||||
background: var(--color-danger-light);
|
|
||||||
color: var(--color-danger-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-featured {
|
|
||||||
background: linear-gradient(135deg, var(--color-primary-100), var(--color-accent-100));
|
|
||||||
color: var(--color-primary-800);
|
|
||||||
border: 1px solid var(--color-primary-200);
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-vip {
|
|
||||||
background: linear-gradient(135deg, #fef3c7, #fde68a);
|
|
||||||
color: #92400e;
|
|
||||||
border: 1px solid #fbbf24;
|
|
||||||
box-shadow: var(--shadow-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced Navigation */
|
|
||||||
.nav {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
border-bottom: 1px solid var(--color-neutral-200);
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
padding: var(--space-3) var(--space-4);
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--color-neutral-600);
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
transition: all var(--duration-normal) var(--ease-in-out);
|
|
||||||
font-family: var(--font-display);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link:hover,
|
|
||||||
.nav-link.active {
|
|
||||||
color: var(--color-primary-600);
|
|
||||||
background: var(--color-primary-50);
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced Layout */
|
|
||||||
.container {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
.container { padding: 0 var(--space-6); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.container { padding: 0 var(--space-8); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-responsive {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--space-6);
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
.grid-responsive { grid-template-columns: repeat(2, 1fr); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.grid-responsive { grid-template-columns: repeat(3, 1fr); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-events {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--space-8);
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
.grid-events { grid-template-columns: repeat(2, 1fr); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.grid-events { grid-template-columns: repeat(3, 1fr); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
|
||||||
.grid-events { grid-template-columns: repeat(4, 1fr); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced animations */
|
|
||||||
@keyframes fadeInUp {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideInRight {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(20px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shimmer {
|
|
||||||
0% {
|
|
||||||
background-position: -200% 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 200% 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-fadeInUp {
|
|
||||||
animation: fadeInUp 0.6s var(--ease-out);
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-slideInRight {
|
|
||||||
animation: slideInRight 0.4s var(--ease-out);
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-pulse {
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-shimmer {
|
|
||||||
background: linear-gradient(90deg, var(--color-neutral-100) 25%, var(--color-neutral-200) 50%, var(--color-neutral-100) 75%);
|
|
||||||
background-size: 200% 100%;
|
|
||||||
animation: shimmer 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced hover effects */
|
|
||||||
.hover-lift {
|
|
||||||
transition: transform var(--duration-normal) var(--ease-out);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-lift:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-glow {
|
|
||||||
transition: all var(--duration-normal) var(--ease-out);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-glow:hover {
|
|
||||||
box-shadow: var(--shadow-purple-lg);
|
|
||||||
filter: brightness(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Focus states with better accessibility */
|
|
||||||
.focus-ring {
|
|
||||||
transition: all var(--duration-fast) var(--ease-out);
|
|
||||||
}
|
|
||||||
|
|
||||||
.focus-ring:focus {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 3px rgb(168 85 247 / 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode enhancements */
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--color-background: var(--color-neutral-900);
|
|
||||||
--color-surface: var(--color-neutral-800);
|
|
||||||
--color-border: var(--color-neutral-700);
|
|
||||||
--color-text-primary: var(--color-neutral-50);
|
|
||||||
--color-text-secondary: var(--color-neutral-300);
|
|
||||||
--color-text-muted: var(--color-neutral-500);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: var(--color-neutral-800);
|
|
||||||
border-color: var(--color-neutral-700);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input,
|
|
||||||
.form-select,
|
|
||||||
.form-textarea {
|
|
||||||
background: var(--color-neutral-700);
|
|
||||||
border-color: var(--color-neutral-600);
|
|
||||||
color: var(--color-neutral-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav {
|
|
||||||
background: rgba(30, 41, 59, 0.95);
|
|
||||||
border-bottom-color: var(--color-neutral-700);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Print styles */
|
|
||||||
@media print {
|
|
||||||
.btn,
|
|
||||||
.nav,
|
|
||||||
.card:hover {
|
|
||||||
box-shadow: none !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-card {
|
|
||||||
break-inside: avoid;
|
|
||||||
margin-bottom: var(--space-4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
53
.superdesign/design_iterations/festival_theme.css
Normal file
53
.superdesign/design_iterations/festival_theme.css
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
: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);
|
||||||
|
}
|
||||||
538
.superdesign/design_iterations/festival_ticket_page.html
Normal file
538
.superdesign/design_iterations/festival_ticket_page.html
Normal file
@@ -0,0 +1,538 @@
|
|||||||
|
<!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,627 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>APERONIGHT - RADICAL EVENT BOOKING</title>
|
|
||||||
<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=Space+Grotesk:wght@400;700;900&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
|
|
||||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
|
||||||
<link rel="stylesheet" href="neo_brutalist_theme.css">
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
line-height: 1.4;
|
|
||||||
color: var(--foreground);
|
|
||||||
background: var(--background);
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
background: repeating-linear-gradient(
|
|
||||||
45deg,
|
|
||||||
var(--secondary) 0px,
|
|
||||||
var(--secondary) 20px,
|
|
||||||
transparent 20px,
|
|
||||||
transparent 40px
|
|
||||||
), var(--background);
|
|
||||||
padding: 100px 0;
|
|
||||||
text-align: center;
|
|
||||||
position: relative;
|
|
||||||
border-bottom: 6px solid var(--border);
|
|
||||||
box-shadow: 0 6px 0px 0px var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
left: 20px;
|
|
||||||
right: 20px;
|
|
||||||
bottom: 20px;
|
|
||||||
border: 4px solid var(--border);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero h1 {
|
|
||||||
font-size: clamp(2.5rem, 8vw, 6rem);
|
|
||||||
font-weight: 900;
|
|
||||||
margin-bottom: var(--space-8);
|
|
||||||
color: var(--foreground);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
text-shadow: 4px 4px 0px var(--accent);
|
|
||||||
animation: glitch 3s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glitch {
|
|
||||||
0%, 100% { transform: translateX(0); }
|
|
||||||
20% { transform: translateX(-2px); }
|
|
||||||
40% { transform: translateX(2px); }
|
|
||||||
60% { transform: translateX(-1px); }
|
|
||||||
80% { transform: translateX(1px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero p {
|
|
||||||
font-size: var(--text-2xl);
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: var(--space-12);
|
|
||||||
max-width: 800px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-section {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-8);
|
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 var(--space-4);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
padding: var(--space-20) 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section:nth-child(odd) {
|
|
||||||
background: linear-gradient(135deg, var(--secondary) 25%, transparent 25%),
|
|
||||||
linear-gradient(225deg, var(--secondary) 25%, transparent 25%),
|
|
||||||
linear-gradient(45deg, var(--secondary) 25%, transparent 25%),
|
|
||||||
linear-gradient(315deg, var(--secondary) 25%, var(--background) 25%);
|
|
||||||
background-size: 40px 40px;
|
|
||||||
background-position: 0 0, 0 20px, 20px -20px, -20px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: clamp(2rem, 6vw, 4rem);
|
|
||||||
font-weight: 900;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: var(--space-16);
|
|
||||||
color: var(--foreground);
|
|
||||||
text-transform: uppercase;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title::after {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
width: 100px;
|
|
||||||
height: 8px;
|
|
||||||
background: var(--accent);
|
|
||||||
margin: var(--space-4) auto 0;
|
|
||||||
box-shadow: var(--shadow-brutal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.events-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
||||||
gap: var(--space-12);
|
|
||||||
margin-bottom: var(--space-16);
|
|
||||||
}
|
|
||||||
|
|
||||||
.brutal-event-card {
|
|
||||||
background: var(--background);
|
|
||||||
border: 4px solid var(--border);
|
|
||||||
position: relative;
|
|
||||||
transition: all 0.1s ease;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brutal-event-card::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -8px;
|
|
||||||
left: -8px;
|
|
||||||
right: -20px;
|
|
||||||
bottom: -20px;
|
|
||||||
background: var(--primary);
|
|
||||||
z-index: -1;
|
|
||||||
transition: all 0.1s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brutal-event-card:hover {
|
|
||||||
transform: translate(8px, 8px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.brutal-event-card:hover::before {
|
|
||||||
transform: translate(-8px, -8px);
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-image {
|
|
||||||
width: 100%;
|
|
||||||
height: 250px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-bottom: 4px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-content {
|
|
||||||
padding: var(--space-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-title {
|
|
||||||
font-size: var(--text-2xl);
|
|
||||||
font-weight: 900;
|
|
||||||
margin-bottom: var(--space-4);
|
|
||||||
color: var(--foreground);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-date {
|
|
||||||
color: var(--foreground);
|
|
||||||
font-size: var(--text-lg);
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: var(--space-4);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-3);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-description {
|
|
||||||
color: var(--foreground);
|
|
||||||
margin-bottom: var(--space-6);
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-price {
|
|
||||||
font-weight: 900;
|
|
||||||
font-size: var(--text-2xl);
|
|
||||||
color: var(--foreground);
|
|
||||||
background: var(--secondary);
|
|
||||||
padding: var(--space-2) var(--space-4);
|
|
||||||
border: 3px solid var(--border);
|
|
||||||
box-shadow: var(--shadow-brutal);
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.features-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
gap: var(--space-12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.brutal-feature {
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--space-8);
|
|
||||||
background: var(--background);
|
|
||||||
border: 4px solid var(--border);
|
|
||||||
position: relative;
|
|
||||||
box-shadow: var(--shadow-brutal-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.brutal-feature::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -12px;
|
|
||||||
left: -12px;
|
|
||||||
right: -24px;
|
|
||||||
bottom: -24px;
|
|
||||||
background: repeating-linear-gradient(
|
|
||||||
45deg,
|
|
||||||
var(--accent) 0px,
|
|
||||||
var(--accent) 10px,
|
|
||||||
transparent 10px,
|
|
||||||
transparent 20px
|
|
||||||
);
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-icon {
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
background: var(--primary);
|
|
||||||
color: white;
|
|
||||||
border: 4px solid var(--border);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0 auto var(--space-6);
|
|
||||||
box-shadow: var(--shadow-brutal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.brutal-feature h3 {
|
|
||||||
font-size: var(--text-2xl);
|
|
||||||
font-weight: 900;
|
|
||||||
margin-bottom: var(--space-4);
|
|
||||||
color: var(--foreground);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brutal-feature p {
|
|
||||||
color: var(--foreground);
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats {
|
|
||||||
background: var(--foreground);
|
|
||||||
color: var(--background);
|
|
||||||
padding: var(--space-20) 0;
|
|
||||||
border-top: 6px solid var(--accent);
|
|
||||||
border-bottom: 6px solid var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: var(--space-12);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item {
|
|
||||||
padding: var(--space-8);
|
|
||||||
background: var(--background);
|
|
||||||
color: var(--foreground);
|
|
||||||
border: 4px solid var(--background);
|
|
||||||
box-shadow: var(--shadow-brutal-xl);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -8px;
|
|
||||||
left: -8px;
|
|
||||||
right: -16px;
|
|
||||||
bottom: -16px;
|
|
||||||
background: var(--accent);
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-number {
|
|
||||||
font-size: clamp(2.5rem, 6vw, 4rem);
|
|
||||||
font-weight: 900;
|
|
||||||
color: var(--primary);
|
|
||||||
display: block;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
color: var(--foreground);
|
|
||||||
margin-top: var(--space-2);
|
|
||||||
font-weight: 900;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
font-size: var(--text-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
background: var(--border);
|
|
||||||
color: var(--background);
|
|
||||||
padding: var(--space-16) 0;
|
|
||||||
border-top: 6px solid var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-content {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: var(--space-8);
|
|
||||||
margin-bottom: var(--space-8);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links a {
|
|
||||||
color: var(--background);
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 900;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
padding: var(--space-2) var(--space-4);
|
|
||||||
border: 3px solid var(--background);
|
|
||||||
transition: all 0.1s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links a:hover {
|
|
||||||
background: var(--background);
|
|
||||||
color: var(--border);
|
|
||||||
box-shadow: 4px 4px 0px 0px var(--accent);
|
|
||||||
transform: translate(-2px, -2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.noise-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
opacity: 0.03;
|
|
||||||
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='1' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.cta-section {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.events-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: var(--space-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: var(--space-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="noise-overlay"></div>
|
|
||||||
|
|
||||||
<!-- Navigation -->
|
|
||||||
<nav class="nav">
|
|
||||||
<div class="container">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: var(--space-4) 0;">
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-4);">
|
|
||||||
<div style="width: 50px; height: 50px; background: var(--primary); border: 4px solid var(--border); display: flex; align-items: center; justify-content: center; color: white; font-weight: 900; box-shadow: var(--shadow-brutal); font-size: var(--text-xl);">A</div>
|
|
||||||
<span style="font-size: var(--text-2xl); font-weight: 900; color: var(--foreground); text-transform: uppercase; letter-spacing: -0.02em;">APERONIGHT</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: var(--space-4); align-items: center;">
|
|
||||||
<a href="#" class="nav-link">EVENTS</a>
|
|
||||||
<a href="#" class="nav-link">ABOUT</a>
|
|
||||||
<a href="#" class="nav-link">CONTACT</a>
|
|
||||||
<button class="btn-primary">SIGN IN</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Hero Section -->
|
|
||||||
<section class="hero">
|
|
||||||
<div class="container">
|
|
||||||
<h1 class="glitch-text" data-text="RADICAL EVENTS">RADICAL EVENTS</h1>
|
|
||||||
<p>BREAK THE BORING. JOIN THE REVOLUTION. EXPERIENCE EVENTS THAT MATTER.</p>
|
|
||||||
<div class="cta-section">
|
|
||||||
<button class="btn-primary">FIND EVENTS</button>
|
|
||||||
<div style="width: 4px; height: 60px; background: var(--border); box-shadow: var(--shadow-brutal);"></div>
|
|
||||||
<button class="btn-secondary">HOST EVENT</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Featured Events -->
|
|
||||||
<section class="section">
|
|
||||||
<div class="container">
|
|
||||||
<h2 class="section-title">FEATURED CHAOS</h2>
|
|
||||||
<div class="events-grid">
|
|
||||||
<div class="brutal-event-card">
|
|
||||||
<img src="https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=400&h=250&fit=crop" alt="TECH CHAOS" class="event-image">
|
|
||||||
<div class="event-content">
|
|
||||||
<h3 class="event-title">TECH CHAOS NIGHT</h3>
|
|
||||||
<div class="event-date">
|
|
||||||
<i data-lucide="zap" style="width: 24px; height: 24px;"></i>
|
|
||||||
THU MAR 15 • 6PM
|
|
||||||
</div>
|
|
||||||
<p class="event-description">SMASH NETWORKING BARRIERS. CODE. DRINKS. CHAOS. REPEAT.</p>
|
|
||||||
<div class="event-footer">
|
|
||||||
<span class="event-price">€25</span>
|
|
||||||
<div style="display: flex; gap: var(--space-3);">
|
|
||||||
<span class="badge-available">LIVE</span>
|
|
||||||
<button class="btn-destructive" style="padding: var(--space-3) var(--space-4);">GRAB IT</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="brutal-event-card">
|
|
||||||
<img src="https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=400&h=250&fit=crop" alt="SOUND ASSAULT" class="event-image">
|
|
||||||
<div class="event-content">
|
|
||||||
<h3 class="event-title">SOUND ASSAULT</h3>
|
|
||||||
<div class="event-date">
|
|
||||||
<i data-lucide="volume-2" style="width: 24px; height: 24px;"></i>
|
|
||||||
SAT MAR 18 • 8PM
|
|
||||||
</div>
|
|
||||||
<p class="event-description">UNDERGROUND BEATS. ROOF ACCESS. CITY DOMINATION.</p>
|
|
||||||
<div class="event-footer">
|
|
||||||
<span class="event-price">€35</span>
|
|
||||||
<div style="display: flex; gap: var(--space-3);">
|
|
||||||
<span class="badge-featured">★ HOT</span>
|
|
||||||
<button class="btn-primary" style="padding: var(--space-3) var(--space-4);">INVADE</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="brutal-event-card">
|
|
||||||
<img src="https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=400&h=250&fit=crop" alt="ART REBELLION" class="event-image">
|
|
||||||
<div class="event-content">
|
|
||||||
<h3 class="event-title">ART REBELLION</h3>
|
|
||||||
<div class="event-date">
|
|
||||||
<i data-lucide="paintbrush" style="width: 24px; height: 24px;"></i>
|
|
||||||
FRI MAR 22 • 7PM
|
|
||||||
</div>
|
|
||||||
<p class="event-description">DESTROY CONVENTIONS. CREATE CHAOS. WINE INCLUDED.</p>
|
|
||||||
<div class="event-footer">
|
|
||||||
<span class="event-price">€20</span>
|
|
||||||
<div style="display: flex; gap: var(--space-3);">
|
|
||||||
<span class="badge-sold-out">DANGER</span>
|
|
||||||
<button class="btn-secondary" style="padding: var(--space-3) var(--space-4);">RISK IT</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="text-align: center;">
|
|
||||||
<button class="btn-secondary" style="font-size: var(--text-xl); padding: var(--space-4) var(--space-8);">MORE CHAOS</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Features -->
|
|
||||||
<section class="section">
|
|
||||||
<div class="container">
|
|
||||||
<h2 class="section-title">WHY WE RULE</h2>
|
|
||||||
<div class="features-grid">
|
|
||||||
<div class="brutal-feature">
|
|
||||||
<div class="feature-icon">
|
|
||||||
<i data-lucide="skull" style="width: 40px; height: 40px;"></i>
|
|
||||||
</div>
|
|
||||||
<h3>CURATED MADNESS</h3>
|
|
||||||
<p>HANDPICKED EVENTS THAT DESTROY BORING AND CREATE LEGENDS.</p>
|
|
||||||
</div>
|
|
||||||
<div class="brutal-feature">
|
|
||||||
<div class="feature-icon">
|
|
||||||
<i data-lucide="shield" style="width: 40px; height: 40px;"></i>
|
|
||||||
</div>
|
|
||||||
<h3>BULLETPROOF BOOKING</h3>
|
|
||||||
<p>SECURE PAYMENTS. INSTANT TICKETS. NO BULLSHIT REFUNDS.</p>
|
|
||||||
</div>
|
|
||||||
<div class="brutal-feature">
|
|
||||||
<div class="feature-icon">
|
|
||||||
<i data-lucide="rocket" style="width: 40px; height: 40px;"></i>
|
|
||||||
</div>
|
|
||||||
<h3>ZERO FRICTION</h3>
|
|
||||||
<p>FIND EVENT. BOOK TICKET. DESTROY EXPECTATIONS. REPEAT.</p>
|
|
||||||
</div>
|
|
||||||
<div class="brutal-feature">
|
|
||||||
<div class="feature-icon">
|
|
||||||
<i data-lucide="users" style="width: 40px; height: 40px;"></i>
|
|
||||||
</div>
|
|
||||||
<h3>TRIBE BUILDING</h3>
|
|
||||||
<p>CONNECT WITH REBELS WHO GET IT. BUILD YOUR EMPIRE.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Stats -->
|
|
||||||
<section class="stats">
|
|
||||||
<div class="container">
|
|
||||||
<div class="stats-grid">
|
|
||||||
<div class="stat-item">
|
|
||||||
<span class="stat-number">50+</span>
|
|
||||||
<div class="stat-label">EVENTS MONTHLY</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<span class="stat-number">2.5K</span>
|
|
||||||
<div class="stat-label">REBELS JOINED</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<span class="stat-number">100+</span>
|
|
||||||
<div class="stat-label">VENUES CONQUERED</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<span class="stat-number">95%</span>
|
|
||||||
<div class="stat-label">MINDS BLOWN</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<footer class="footer">
|
|
||||||
<div class="container">
|
|
||||||
<div class="footer-content">
|
|
||||||
<div class="footer-links">
|
|
||||||
<a href="#">ABOUT</a>
|
|
||||||
<a href="#">EVENTS</a>
|
|
||||||
<a href="#">SUPPORT</a>
|
|
||||||
<a href="#">PRIVACY</a>
|
|
||||||
<a href="#">TERMS</a>
|
|
||||||
</div>
|
|
||||||
<p style="font-weight: 900; text-transform: uppercase; letter-spacing: 0.1em;">© 2024 APERONIGHT. CHAOS RESERVED.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Initialize Lucide icons
|
|
||||||
lucide.createIcons();
|
|
||||||
|
|
||||||
// Add brutal animations on scroll
|
|
||||||
const observerOptions = {
|
|
||||||
threshold: 0.2,
|
|
||||||
rootMargin: '0px 0px -100px 0px'
|
|
||||||
};
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
entry.target.style.animation = 'none';
|
|
||||||
entry.target.offsetHeight; // Trigger reflow
|
|
||||||
entry.target.style.animation = 'shake-brutal 0.5s ease-in-out';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, observerOptions);
|
|
||||||
|
|
||||||
document.querySelectorAll('.brutal-event-card, .brutal-feature, .stat-item').forEach(el => {
|
|
||||||
observer.observe(el);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add random glitch effects
|
|
||||||
setInterval(() => {
|
|
||||||
const elements = document.querySelectorAll('.section-title, .event-title');
|
|
||||||
const randomElement = elements[Math.floor(Math.random() * elements.length)];
|
|
||||||
if (randomElement && Math.random() > 0.9) {
|
|
||||||
randomElement.style.animation = 'glitch 0.3s ease-in-out';
|
|
||||||
setTimeout(() => {
|
|
||||||
randomElement.style.animation = '';
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,417 +0,0 @@
|
|||||||
/* Neo-Brutalist Design System for Event Booking */
|
|
||||||
:root {
|
|
||||||
/* Colors - Bold and high contrast */
|
|
||||||
--background: #ffffff;
|
|
||||||
--foreground: #000000;
|
|
||||||
--card: #ffffff;
|
|
||||||
--card-foreground: #000000;
|
|
||||||
--popover: #ffffff;
|
|
||||||
--popover-foreground: #000000;
|
|
||||||
--primary: #ff6b35;
|
|
||||||
--primary-foreground: #ffffff;
|
|
||||||
--secondary: #00f5ff;
|
|
||||||
--secondary-foreground: #000000;
|
|
||||||
--muted: #f0f0f0;
|
|
||||||
--muted-foreground: #333333;
|
|
||||||
--accent: #ff1744;
|
|
||||||
--accent-foreground: #ffffff;
|
|
||||||
--destructive: #000000;
|
|
||||||
--destructive-foreground: #ffffff;
|
|
||||||
--border: #000000;
|
|
||||||
--input: #ffffff;
|
|
||||||
--ring: #ff6b35;
|
|
||||||
|
|
||||||
/* Event-specific colors - Bold and vibrant */
|
|
||||||
--event-featured: #7c4dff;
|
|
||||||
--event-sold-out: #000000;
|
|
||||||
--event-available: #00c853;
|
|
||||||
--ticket-premium: #ffc107;
|
|
||||||
--ticket-standard: #9e9e9e;
|
|
||||||
|
|
||||||
/* Typography - Bold and impactful */
|
|
||||||
--font-sans: 'Space Grotesk', 'Arial Black', sans-serif;
|
|
||||||
--font-mono: 'Space Mono', 'Courier New', monospace;
|
|
||||||
|
|
||||||
/* Font sizes - Exaggerated scale */
|
|
||||||
--text-xs: 0.75rem;
|
|
||||||
--text-sm: 0.875rem;
|
|
||||||
--text-base: 1rem;
|
|
||||||
--text-lg: 1.25rem;
|
|
||||||
--text-xl: 1.5rem;
|
|
||||||
--text-2xl: 2rem;
|
|
||||||
--text-3xl: 2.5rem;
|
|
||||||
--text-4xl: 3.5rem;
|
|
||||||
|
|
||||||
/* Spacing - Generous */
|
|
||||||
--space-1: 0.25rem;
|
|
||||||
--space-2: 0.5rem;
|
|
||||||
--space-3: 0.75rem;
|
|
||||||
--space-4: 1rem;
|
|
||||||
--space-6: 1.5rem;
|
|
||||||
--space-8: 2rem;
|
|
||||||
--space-12: 3rem;
|
|
||||||
--space-16: 4rem;
|
|
||||||
--space-20: 5rem;
|
|
||||||
|
|
||||||
/* Border radius - Sharp edges */
|
|
||||||
--radius: 0px;
|
|
||||||
--radius-sm: 0px;
|
|
||||||
--radius-md: 0px;
|
|
||||||
--radius-lg: 0px;
|
|
||||||
--radius-xl: 0px;
|
|
||||||
|
|
||||||
/* Shadows - Bold and offset */
|
|
||||||
--shadow-brutal: 8px 8px 0px 0px #000000;
|
|
||||||
--shadow-brutal-lg: 12px 12px 0px 0px #000000;
|
|
||||||
--shadow-brutal-xl: 16px 16px 0px 0px #000000;
|
|
||||||
--shadow-brutal-color: 8px 8px 0px 0px var(--accent);
|
|
||||||
--shadow-inset: inset 4px 4px 0px 0px rgba(0,0,0,0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Typography overrides */
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
font-family: var(--font-sans) !important;
|
|
||||||
font-weight: 900 !important;
|
|
||||||
text-transform: uppercase !important;
|
|
||||||
letter-spacing: -0.02em !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 { font-size: var(--text-4xl) !important; }
|
|
||||||
h2 { font-size: var(--text-3xl) !important; }
|
|
||||||
h3 { font-size: var(--text-2xl) !important; }
|
|
||||||
|
|
||||||
/* Component styles */
|
|
||||||
.btn-primary {
|
|
||||||
background: var(--primary);
|
|
||||||
color: var(--primary-foreground);
|
|
||||||
padding: 16px 32px;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
font-weight: 900;
|
|
||||||
border: 3px solid var(--border);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.1s ease;
|
|
||||||
box-shadow: var(--shadow-brutal);
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
transform: translate(4px, 4px);
|
|
||||||
box-shadow: 4px 4px 0px 0px #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:active {
|
|
||||||
transform: translate(8px, 8px);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: var(--secondary);
|
|
||||||
color: var(--secondary-foreground);
|
|
||||||
padding: 16px 32px;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
font-weight: 900;
|
|
||||||
border: 3px solid var(--border);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.1s ease;
|
|
||||||
box-shadow: var(--shadow-brutal);
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
transform: translate(4px, 4px);
|
|
||||||
box-shadow: 4px 4px 0px 0px #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-destructive {
|
|
||||||
background: var(--destructive);
|
|
||||||
color: var(--destructive-foreground);
|
|
||||||
padding: 16px 32px;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
font-weight: 900;
|
|
||||||
border: 3px solid var(--border);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.1s ease;
|
|
||||||
box-shadow: var(--shadow-brutal-color);
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: var(--card);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
box-shadow: var(--shadow-brutal-lg);
|
|
||||||
padding: var(--space-8);
|
|
||||||
border: 4px solid var(--border);
|
|
||||||
transition: all 0.1s ease;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -4px;
|
|
||||||
left: -4px;
|
|
||||||
right: -16px;
|
|
||||||
bottom: -16px;
|
|
||||||
background: var(--accent);
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
transform: translate(4px, 4px);
|
|
||||||
box-shadow: 8px 8px 0px 0px #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-card {
|
|
||||||
background: var(--card);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
overflow: visible;
|
|
||||||
box-shadow: var(--shadow-brutal-xl);
|
|
||||||
transition: all 0.1s ease;
|
|
||||||
border: 4px solid var(--border);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-card::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -8px;
|
|
||||||
left: -8px;
|
|
||||||
right: -24px;
|
|
||||||
bottom: -24px;
|
|
||||||
background: repeating-linear-gradient(
|
|
||||||
45deg,
|
|
||||||
transparent,
|
|
||||||
transparent 10px,
|
|
||||||
var(--secondary) 10px,
|
|
||||||
var(--secondary) 20px
|
|
||||||
);
|
|
||||||
z-index: -1;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-card:hover::after {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-card:hover {
|
|
||||||
transform: translate(8px, 8px);
|
|
||||||
box-shadow: 8px 8px 0px 0px #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticket-card {
|
|
||||||
background: var(--muted);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: var(--space-6);
|
|
||||||
border: 3px solid var(--border);
|
|
||||||
transition: all 0.1s ease;
|
|
||||||
box-shadow: var(--shadow-brutal);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticket-card.selected {
|
|
||||||
background: var(--primary);
|
|
||||||
color: var(--primary-foreground);
|
|
||||||
transform: translate(4px, 4px);
|
|
||||||
box-shadow: 4px 4px 0px 0px #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticket-card.selected::before {
|
|
||||||
content: '✓';
|
|
||||||
position: absolute;
|
|
||||||
top: -10px;
|
|
||||||
right: -10px;
|
|
||||||
background: var(--accent);
|
|
||||||
color: white;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: 900;
|
|
||||||
border: 3px solid var(--border);
|
|
||||||
box-shadow: 4px 4px 0px 0px #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input {
|
|
||||||
background: var(--input);
|
|
||||||
border: 3px solid var(--border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 16px 20px;
|
|
||||||
font-size: var(--text-lg);
|
|
||||||
font-weight: 700;
|
|
||||||
transition: all 0.1s ease;
|
|
||||||
box-shadow: var(--shadow-inset);
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--primary);
|
|
||||||
box-shadow: var(--shadow-brutal-color);
|
|
||||||
transform: translate(-2px, -2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
font-size: var(--text-sm);
|
|
||||||
font-weight: 900;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
border: 2px solid var(--border);
|
|
||||||
box-shadow: 4px 4px 0px 0px #000000;
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-available {
|
|
||||||
background: var(--event-available);
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-sold-out {
|
|
||||||
background: var(--event-sold-out);
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-featured {
|
|
||||||
background: var(--event-featured);
|
|
||||||
color: #ffffff;
|
|
||||||
animation: pulse-brutal 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-premium {
|
|
||||||
background: var(--ticket-premium);
|
|
||||||
color: #000000;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-premium::after {
|
|
||||||
content: '★';
|
|
||||||
margin-left: 8px;
|
|
||||||
animation: rotate 3s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation */
|
|
||||||
.nav {
|
|
||||||
background: var(--background);
|
|
||||||
border-bottom: 6px solid var(--border);
|
|
||||||
box-shadow: 0 6px 0px 0px var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
color: var(--foreground);
|
|
||||||
font-weight: 900;
|
|
||||||
padding: 12px 20px;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
transition: all 0.1s ease;
|
|
||||||
text-decoration: none;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
border: 3px solid transparent;
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link:hover, .nav-link.active {
|
|
||||||
background: var(--primary);
|
|
||||||
color: var(--primary-foreground);
|
|
||||||
border-color: var(--border);
|
|
||||||
box-shadow: 4px 4px 0px 0px #000000;
|
|
||||||
transform: translate(-2px, -2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animations */
|
|
||||||
@keyframes pulse-brutal {
|
|
||||||
0%, 100% {
|
|
||||||
transform: scale(1);
|
|
||||||
box-shadow: 4px 4px 0px 0px #000000;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1.05);
|
|
||||||
box-shadow: 6px 6px 0px 0px #000000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rotate {
|
|
||||||
from { transform: rotate(0deg); }
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shake-brutal {
|
|
||||||
0%, 100% { transform: translateX(0); }
|
|
||||||
25% { transform: translateX(-8px); }
|
|
||||||
75% { transform: translateX(8px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-shake {
|
|
||||||
animation: shake-brutal 0.5s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Special effects */
|
|
||||||
.glitch-text {
|
|
||||||
position: relative;
|
|
||||||
color: var(--primary);
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glitch-text::before,
|
|
||||||
.glitch-text::after {
|
|
||||||
content: attr(data-text);
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
clip-path: polygon(0 0, 100% 0, 100% 45%, 0 45%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.glitch-text::before {
|
|
||||||
color: var(--accent);
|
|
||||||
animation: glitch-1 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glitch-text::after {
|
|
||||||
color: var(--secondary);
|
|
||||||
animation: glitch-2 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glitch-1 {
|
|
||||||
0%, 100% { transform: translateX(0); }
|
|
||||||
20% { transform: translateX(-2px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glitch-2 {
|
|
||||||
0%, 100% { transform: translateX(0); }
|
|
||||||
20% { transform: translateX(2px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive design */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
:root {
|
|
||||||
--space-4: 0.75rem;
|
|
||||||
--space-6: 1rem;
|
|
||||||
--space-8: 1.5rem;
|
|
||||||
--text-4xl: 2.5rem;
|
|
||||||
--text-3xl: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary, .btn-secondary, .btn-destructive {
|
|
||||||
padding: 12px 24px;
|
|
||||||
font-size: var(--text-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadow-brutal, .shadow-brutal-lg, .shadow-brutal-xl {
|
|
||||||
box-shadow: 4px 4px 0px 0px #000000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
480
.superdesign/design_iterations/quantic_login_1.html
Normal file
480
.superdesign/design_iterations/quantic_login_1.html
Normal file
@@ -0,0 +1,480 @@
|
|||||||
|
<!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>
|
||||||
71
.superdesign/design_iterations/quantic_telecom_theme.css
Normal file
71
.superdesign/design_iterations/quantic_telecom_theme.css
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
: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);
|
||||||
|
}
|
||||||
0
.tool-versions
Normal file → Executable file
0
.tool-versions
Normal file → Executable file
0
.windsurfrules
Normal file → Executable file
0
.windsurfrules
Normal file → Executable file
300
AGENT.md
Executable file
300
AGENT.md
Executable file
@@ -0,0 +1,300 @@
|
|||||||
|
# Aperonight - Technical Documentation for AI Agents
|
||||||
|
|
||||||
|
## 🤖 Agent Implementation Guide
|
||||||
|
|
||||||
|
This document provides technical details for AI agents working on the Aperonight ticket selling system.
|
||||||
|
|
||||||
|
## 🏗️ System Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
#### 1. User Management (`app/models/user.rb`)
|
||||||
|
- **Devise Integration**: Complete authentication system with registration, login, password reset
|
||||||
|
- **Relationships**: Users can create events and purchase tickets
|
||||||
|
- **Validations**: Email format, password strength, optional name fields
|
||||||
|
|
||||||
|
#### 2. Event System (`app/models/event.rb`)
|
||||||
|
- **States**: `draft`, `published`, `canceled`, `sold_out` with enum management
|
||||||
|
- **Geographic Data**: Latitude/longitude for venue mapping
|
||||||
|
- **Relationships**: Belongs to user, has many ticket types and tickets through ticket types
|
||||||
|
- **Scopes**: Featured events, published events, upcoming events with proper ordering
|
||||||
|
|
||||||
|
#### 3. Ticket Management
|
||||||
|
- **TicketType** (`app/models/ticket_type.rb`): Defines ticket categories with pricing, quantity, sale periods
|
||||||
|
- **Ticket** (`app/models/ticket.rb`): Individual tickets with unique QR codes, status tracking, price storage
|
||||||
|
|
||||||
|
#### 4. Payment Processing (`app/controllers/events_controller.rb`)
|
||||||
|
- **Stripe Integration**: Complete checkout session creation and payment confirmation
|
||||||
|
- **Session Management**: Proper handling of payment success/failure with ticket generation
|
||||||
|
- **Security**: Authentication required, cart validation, availability checking
|
||||||
|
|
||||||
|
### Database Schema Key Points
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Users table (managed by Devise)
|
||||||
|
CREATE TABLE users (
|
||||||
|
id bigint PRIMARY KEY,
|
||||||
|
email varchar(255) UNIQUE NOT NULL,
|
||||||
|
encrypted_password varchar(255) NOT NULL,
|
||||||
|
first_name varchar(255),
|
||||||
|
last_name varchar(255),
|
||||||
|
-- Devise fields: confirmation, reset tokens, etc.
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Events table
|
||||||
|
CREATE TABLE events (
|
||||||
|
id bigint PRIMARY KEY,
|
||||||
|
user_id bigint REFERENCES users(id),
|
||||||
|
name varchar(100) NOT NULL,
|
||||||
|
slug varchar(100) NOT NULL,
|
||||||
|
description text(1000) NOT NULL,
|
||||||
|
venue_name varchar(100) NOT NULL,
|
||||||
|
venue_address varchar(200) NOT NULL,
|
||||||
|
latitude decimal(10,8) NOT NULL,
|
||||||
|
longitude decimal(11,8) NOT NULL,
|
||||||
|
start_time datetime NOT NULL,
|
||||||
|
end_time datetime,
|
||||||
|
state integer DEFAULT 0, -- enum: draft=0, published=1, canceled=2, sold_out=3
|
||||||
|
featured boolean DEFAULT false,
|
||||||
|
image varchar(500)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Ticket types define pricing and availability
|
||||||
|
CREATE TABLE ticket_types (
|
||||||
|
id bigint PRIMARY KEY,
|
||||||
|
event_id bigint REFERENCES events(id),
|
||||||
|
name varchar(255) NOT NULL,
|
||||||
|
description text,
|
||||||
|
price_cents integer NOT NULL,
|
||||||
|
quantity integer NOT NULL,
|
||||||
|
sale_start_at datetime,
|
||||||
|
sale_end_at datetime,
|
||||||
|
requires_id boolean DEFAULT false,
|
||||||
|
minimum_age integer
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Individual tickets with QR codes
|
||||||
|
CREATE TABLE tickets (
|
||||||
|
id bigint PRIMARY KEY,
|
||||||
|
user_id bigint REFERENCES users(id),
|
||||||
|
ticket_type_id bigint REFERENCES ticket_types(id),
|
||||||
|
qr_code varchar(255) UNIQUE NOT NULL,
|
||||||
|
price_cents integer NOT NULL,
|
||||||
|
status varchar(255) DEFAULT 'active' -- active, used, expired, refunded
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Key Implementation Details
|
||||||
|
|
||||||
|
### 1. Dashboard Metrics (`app/controllers/pages_controller.rb`)
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# User-specific metrics with optimized queries
|
||||||
|
@booked_events = current_user.tickets
|
||||||
|
.joins(:ticket_type, :event)
|
||||||
|
.where(events: { state: :published })
|
||||||
|
.count
|
||||||
|
|
||||||
|
# Event counts for different timeframes
|
||||||
|
@events_today = Event.published
|
||||||
|
.where("DATE(start_time) = ?", Date.current)
|
||||||
|
.count
|
||||||
|
|
||||||
|
# User's actual booked events (not just count)
|
||||||
|
@user_booked_events = Event.joins(ticket_types: :tickets)
|
||||||
|
.where(tickets: { user: current_user, status: 'active' })
|
||||||
|
.distinct
|
||||||
|
.limit(5)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Stripe Payment Flow
|
||||||
|
|
||||||
|
#### Checkout Initiation (`events#checkout`)
|
||||||
|
1. **Cart Validation**: Parse JSON cart data, validate ticket types and quantities
|
||||||
|
2. **Availability Check**: Ensure sufficient tickets available before payment
|
||||||
|
3. **Stripe Session**: Create checkout session with line items, success/cancel URLs
|
||||||
|
4. **Metadata Storage**: Store order details in Stripe session metadata for later retrieval
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Key Stripe configuration
|
||||||
|
session = Stripe::Checkout::Session.create({
|
||||||
|
payment_method_types: ['card'],
|
||||||
|
line_items: line_items,
|
||||||
|
mode: 'payment',
|
||||||
|
success_url: payment_success_url(event_id: @event.id, session_id: '{CHECKOUT_SESSION_ID}'),
|
||||||
|
cancel_url: event_url(@event.slug, @event),
|
||||||
|
customer_email: current_user.email,
|
||||||
|
metadata: {
|
||||||
|
event_id: @event.id,
|
||||||
|
user_id: current_user.id,
|
||||||
|
order_items: order_items.to_json
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Payment Confirmation (`events#payment_success`)
|
||||||
|
1. **Session Retrieval**: Get Stripe session with payment status
|
||||||
|
2. **Ticket Creation**: Generate tickets based on order items from metadata
|
||||||
|
3. **QR Code Generation**: Automatic unique QR code creation via model callbacks
|
||||||
|
4. **Success Page**: Display tickets with download links
|
||||||
|
|
||||||
|
### 3. PDF Ticket Generation (`app/services/ticket_pdf_generator.rb`)
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class TicketPdfGenerator
|
||||||
|
def generate
|
||||||
|
Prawn::Document.new(page_size: [350, 600], margin: 20) do |pdf|
|
||||||
|
# Header with branding
|
||||||
|
pdf.fill_color "2D1B69"
|
||||||
|
pdf.font "Helvetica", style: :bold, size: 24
|
||||||
|
pdf.text "ApéroNight", align: :center
|
||||||
|
|
||||||
|
# Event details
|
||||||
|
pdf.text ticket.event.name, align: :center
|
||||||
|
|
||||||
|
# QR Code generation
|
||||||
|
qr_code_data = {
|
||||||
|
ticket_id: ticket.id,
|
||||||
|
qr_code: ticket.qr_code,
|
||||||
|
event_id: ticket.event.id,
|
||||||
|
user_id: ticket.user.id
|
||||||
|
}.to_json
|
||||||
|
|
||||||
|
qrcode = RQRCode::QRCode.new(qr_code_data)
|
||||||
|
pdf.print_qr_code(qrcode, extent: 120, align: :center)
|
||||||
|
end.render
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Frontend Cart Management (`app/javascript/controllers/ticket_cart_controller.js`)
|
||||||
|
|
||||||
|
- **Stimulus Controller**: Manages cart state and interactions
|
||||||
|
- **Authentication Check**: Validates user login before checkout
|
||||||
|
- **Session Storage**: Preserves cart when redirecting to login
|
||||||
|
- **Dynamic Updates**: Real-time cart total and ticket count updates
|
||||||
|
|
||||||
|
## 🔧 Development Patterns
|
||||||
|
|
||||||
|
### Model Validations
|
||||||
|
```ruby
|
||||||
|
# Event validations
|
||||||
|
validates :name, presence: true, length: { minimum: 3, maximum: 100 }
|
||||||
|
validates :latitude, numericality: {
|
||||||
|
greater_than_or_equal_to: -90,
|
||||||
|
less_than_or_equal_to: 90
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ticket QR code generation
|
||||||
|
before_validation :generate_qr_code, on: :create
|
||||||
|
def generate_qr_code
|
||||||
|
loop do
|
||||||
|
self.qr_code = SecureRandom.uuid
|
||||||
|
break unless Ticket.exists?(qr_code: qr_code)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Controller Patterns
|
||||||
|
```ruby
|
||||||
|
# Authentication for sensitive actions
|
||||||
|
before_action :authenticate_user!, only: [:checkout, :payment_success, :download_ticket]
|
||||||
|
|
||||||
|
# Strong parameters
|
||||||
|
private
|
||||||
|
def event_params
|
||||||
|
params.require(:event).permit(:name, :description, :venue_name, :venue_address,
|
||||||
|
:latitude, :longitude, :start_time, :image)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Helpers and Partials
|
||||||
|
- **Metric Cards**: Reusable component for dashboard statistics
|
||||||
|
- **Event Items**: Consistent event display across pages
|
||||||
|
- **Flash Messages**: Centralized notification system
|
||||||
|
|
||||||
|
## 🚀 Deployment Considerations
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
```bash
|
||||||
|
# Required for production
|
||||||
|
STRIPE_PUBLISHABLE_KEY=pk_live_...
|
||||||
|
STRIPE_SECRET_KEY=sk_live_...
|
||||||
|
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||||
|
DATABASE_URL=mysql2://user:pass@host/db
|
||||||
|
RAILS_MASTER_KEY=...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Indexes
|
||||||
|
```sql
|
||||||
|
-- Performance indexes for common queries
|
||||||
|
CREATE INDEX idx_events_published_start_time ON events (state, start_time);
|
||||||
|
CREATE INDEX idx_tickets_user_status ON tickets (user_id, status);
|
||||||
|
CREATE INDEX idx_ticket_types_event ON ticket_types (event_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
- **CSRF Protection**: Rails default protection enabled
|
||||||
|
- **Strong Parameters**: All user inputs filtered
|
||||||
|
- **Authentication**: Devise handles session security
|
||||||
|
- **Payment Security**: Stripe handles sensitive payment data
|
||||||
|
|
||||||
|
## 🧪 Testing Strategy
|
||||||
|
|
||||||
|
### Key Test Cases
|
||||||
|
1. **User Authentication**: Registration, login, logout flows
|
||||||
|
2. **Event Creation**: Validation, state management, relationships
|
||||||
|
3. **Booking Process**: Cart validation, payment processing, ticket generation
|
||||||
|
4. **PDF Generation**: QR code uniqueness, ticket format
|
||||||
|
5. **Dashboard Metrics**: Query accuracy, performance
|
||||||
|
|
||||||
|
### Seed Data Structure
|
||||||
|
```ruby
|
||||||
|
# Creates test users, events, and ticket types
|
||||||
|
users = User.create!([...])
|
||||||
|
events = Event.create!([...])
|
||||||
|
ticket_types = TicketType.create!([...])
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Available Development Tools
|
||||||
|
|
||||||
|
### AST-Grep for Mass Code Replacement
|
||||||
|
|
||||||
|
The system has `ast-grep` installed for structural code search and replacement. This tool is particularly useful for:
|
||||||
|
|
||||||
|
- **Mass refactoring**: Rename methods, classes, or variables across the codebase
|
||||||
|
- **Pattern-based replacements**: Update code patterns using AST matching
|
||||||
|
- **Language-aware transformations**: Safer than regex for code modifications
|
||||||
|
|
||||||
|
#### Usage Examples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find all method calls to a specific method
|
||||||
|
ast-grep --pattern 'find_by_$FIELD($VALUE)' --lang ruby
|
||||||
|
|
||||||
|
# Replace method calls with new syntax
|
||||||
|
ast-grep --pattern 'find_by_$FIELD($VALUE)' --rewrite 'find_by($FIELD: $VALUE)' --lang ruby
|
||||||
|
|
||||||
|
# Search for specific Rails patterns
|
||||||
|
ast-grep --pattern 'validates :$FIELD, presence: true' --lang ruby
|
||||||
|
|
||||||
|
# Mass rename across multiple files
|
||||||
|
ast-grep --pattern 'old_method_name($$$ARGS)' --rewrite 'new_method_name($$$ARGS)' --lang ruby --update-all
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Best Practices:
|
||||||
|
- Always run with `--dry-run` first to preview changes
|
||||||
|
- Use `--lang ruby` for Ruby files to ensure proper AST parsing
|
||||||
|
- Test changes in a branch before applying to main codebase
|
||||||
|
- Particularly useful for Rails conventions and ActiveRecord pattern updates
|
||||||
|
|
||||||
|
## 📝 Code Style & Conventions
|
||||||
|
|
||||||
|
- **Ruby Style**: Follow Rails conventions and Rubocop rules
|
||||||
|
- **Database**: Use Rails migrations for all schema changes
|
||||||
|
- **JavaScript**: Stimulus controllers for interactive behavior
|
||||||
|
- **CSS**: Tailwind utility classes with custom components
|
||||||
|
- **Documentation**: Inline comments for complex business logic
|
||||||
|
- **Mass Changes**: Use `ast-grep` for structural code replacements instead of simple find/replace
|
||||||
|
|
||||||
|
This architecture provides a solid foundation for a scalable ticket selling platform with proper separation of concerns, security, and user experience.
|
||||||
44
BACKLOG.md
Executable file
44
BACKLOG.md
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
# Project Backlog
|
||||||
|
|
||||||
|
## 📋 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
|
||||||
|
|
||||||
|
## 🚧 Doing
|
||||||
|
|
||||||
|
- [ ] refactor: Moving checkout to OrdersController
|
||||||
|
|
||||||
|
## ✅ 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
|
||||||
0
Dockerfile
Normal file → Executable file
0
Dockerfile
Normal file → Executable file
16
Gemfile
Normal file → Executable file
16
Gemfile
Normal file → Executable file
@@ -57,6 +57,9 @@ group :development, :test do
|
|||||||
|
|
||||||
# Improve Minitest output
|
# Improve Minitest output
|
||||||
gem "minitest-reporters", "~> 1.7"
|
gem "minitest-reporters", "~> 1.7"
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
gem "dotenv-rails"
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
@@ -68,6 +71,10 @@ group :test do
|
|||||||
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
|
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
|
||||||
gem "capybara"
|
gem "capybara"
|
||||||
gem "selenium-webdriver"
|
gem "selenium-webdriver"
|
||||||
|
# For controller testing helpers
|
||||||
|
gem "rails-controller-testing"
|
||||||
|
# For mocking and stubbing
|
||||||
|
gem "mocha"
|
||||||
end
|
end
|
||||||
|
|
||||||
gem "devise", "~> 4.9"
|
gem "devise", "~> 4.9"
|
||||||
@@ -76,4 +83,13 @@ gem "devise", "~> 4.9"
|
|||||||
gem "kaminari", "~> 1.2"
|
gem "kaminari", "~> 1.2"
|
||||||
gem "kaminari-tailwind", "~> 0.1.0"
|
gem "kaminari-tailwind", "~> 0.1.0"
|
||||||
|
|
||||||
|
# Stripe payment processing
|
||||||
|
gem "stripe", "~> 15.5"
|
||||||
|
|
||||||
|
# PDF generation for tickets
|
||||||
|
gem "grover"
|
||||||
|
|
||||||
|
# QR code generation
|
||||||
|
gem "rqrcode", "~> 3.1"
|
||||||
|
|
||||||
# gem "net-pop", "~> 0.1.2"
|
# gem "net-pop", "~> 0.1.2"
|
||||||
|
|||||||
28
Gemfile.lock
Normal file → Executable file
28
Gemfile.lock
Normal file → Executable file
@@ -96,6 +96,7 @@ GEM
|
|||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
regexp_parser (>= 1.5, < 3.0)
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
|
chunky_png (1.4.0)
|
||||||
concurrent-ruby (1.3.5)
|
concurrent-ruby (1.3.5)
|
||||||
connection_pool (2.5.3)
|
connection_pool (2.5.3)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
@@ -112,6 +113,9 @@ GEM
|
|||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
dotenv (3.1.8)
|
dotenv (3.1.8)
|
||||||
|
dotenv-rails (3.1.8)
|
||||||
|
dotenv (= 3.1.8)
|
||||||
|
railties (>= 6.1)
|
||||||
drb (2.2.3)
|
drb (2.2.3)
|
||||||
ed25519 (1.4.0)
|
ed25519 (1.4.0)
|
||||||
erb (5.0.2)
|
erb (5.0.2)
|
||||||
@@ -123,6 +127,8 @@ GEM
|
|||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
|
grover (1.2.3)
|
||||||
|
nokogiri (~> 1)
|
||||||
i18n (1.14.7)
|
i18n (1.14.7)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
io-console (0.8.1)
|
io-console (0.8.1)
|
||||||
@@ -180,6 +186,8 @@ GEM
|
|||||||
builder
|
builder
|
||||||
minitest (>= 5.0)
|
minitest (>= 5.0)
|
||||||
ruby-progressbar
|
ruby-progressbar
|
||||||
|
mocha (2.7.1)
|
||||||
|
ruby2_keywords (>= 0.0.5)
|
||||||
msgpack (1.8.0)
|
msgpack (1.8.0)
|
||||||
mysql2 (0.5.6)
|
mysql2 (0.5.6)
|
||||||
net-imap (0.5.9)
|
net-imap (0.5.9)
|
||||||
@@ -253,6 +261,10 @@ GEM
|
|||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.0.2.1)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 8.0.2.1)
|
railties (= 8.0.2.1)
|
||||||
|
rails-controller-testing (1.0.5)
|
||||||
|
actionpack (>= 5.0.1.rc1)
|
||||||
|
actionview (>= 5.0.1.rc1)
|
||||||
|
activesupport (>= 5.0.1.rc1)
|
||||||
rails-dom-testing (2.3.0)
|
rails-dom-testing (2.3.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
@@ -279,8 +291,12 @@ GEM
|
|||||||
responders (3.1.1)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml (3.4.1)
|
rexml (3.4.2)
|
||||||
rubocop (1.80.0)
|
rqrcode (3.1.0)
|
||||||
|
chunky_png (~> 1.0)
|
||||||
|
rqrcode_core (~> 2.0)
|
||||||
|
rqrcode_core (2.0.0)
|
||||||
|
rubocop (1.80.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (~> 3.17.0.2)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
lint_roller (~> 1.1.0)
|
lint_roller (~> 1.1.0)
|
||||||
@@ -309,6 +325,7 @@ GEM
|
|||||||
rubocop-performance (>= 1.24)
|
rubocop-performance (>= 1.24)
|
||||||
rubocop-rails (>= 2.30)
|
rubocop-rails (>= 2.30)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (3.0.2)
|
rubyzip (3.0.2)
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
selenium-webdriver (4.35.0)
|
selenium-webdriver (4.35.0)
|
||||||
@@ -349,6 +366,7 @@ GEM
|
|||||||
stimulus-rails (1.3.4)
|
stimulus-rails (1.3.4)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
stringio (3.1.7)
|
stringio (3.1.7)
|
||||||
|
stripe (15.5.0)
|
||||||
thor (1.4.0)
|
thor (1.4.0)
|
||||||
thruster (0.1.15)
|
thruster (0.1.15)
|
||||||
thruster (0.1.15-aarch64-linux)
|
thruster (0.1.15-aarch64-linux)
|
||||||
@@ -396,16 +414,21 @@ DEPENDENCIES
|
|||||||
cssbundling-rails
|
cssbundling-rails
|
||||||
debug
|
debug
|
||||||
devise (~> 4.9)
|
devise (~> 4.9)
|
||||||
|
dotenv-rails
|
||||||
|
grover
|
||||||
jbuilder
|
jbuilder
|
||||||
jsbundling-rails
|
jsbundling-rails
|
||||||
kamal
|
kamal
|
||||||
kaminari (~> 1.2)
|
kaminari (~> 1.2)
|
||||||
kaminari-tailwind (~> 0.1.0)
|
kaminari-tailwind (~> 0.1.0)
|
||||||
minitest-reporters (~> 1.7)
|
minitest-reporters (~> 1.7)
|
||||||
|
mocha
|
||||||
mysql2 (~> 0.5)
|
mysql2 (~> 0.5)
|
||||||
propshaft
|
propshaft
|
||||||
puma (>= 5.0)
|
puma (>= 5.0)
|
||||||
rails (~> 8.0.2, >= 8.0.2.1)
|
rails (~> 8.0.2, >= 8.0.2.1)
|
||||||
|
rails-controller-testing
|
||||||
|
rqrcode (~> 3.1)
|
||||||
rubocop-rails-omakase
|
rubocop-rails-omakase
|
||||||
selenium-webdriver
|
selenium-webdriver
|
||||||
solid_cable
|
solid_cable
|
||||||
@@ -413,6 +436,7 @@ DEPENDENCIES
|
|||||||
solid_queue
|
solid_queue
|
||||||
sqlite3 (~> 2.7)
|
sqlite3 (~> 2.7)
|
||||||
stimulus-rails
|
stimulus-rails
|
||||||
|
stripe (~> 15.5)
|
||||||
thruster
|
thruster
|
||||||
turbo-rails
|
turbo-rails
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
|
|||||||
0
Procfile.dev
Normal file → Executable file
0
Procfile.dev
Normal file → Executable file
25
QWEN.md
25
QWEN.md
@@ -1,25 +0,0 @@
|
|||||||
# Qwen Code Customization
|
|
||||||
|
|
||||||
## Project Context
|
|
||||||
- Working on a Ruby on Rails project named "aperonight"
|
|
||||||
- Using Docker for containerization
|
|
||||||
- Following Ruby version 3.1.0 (as indicated by .ruby-version)
|
|
||||||
- Using Bundler for gem management (Gemfile)
|
|
||||||
- Using Node.js for frontend assets (package.json likely present)
|
|
||||||
|
|
||||||
## Preferences
|
|
||||||
- Prefer to use Ruby and Rails conventions
|
|
||||||
- Follow Docker best practices for development environments
|
|
||||||
- Use standard Ruby/Rails project structure
|
|
||||||
- When creating new files, follow Rails conventions
|
|
||||||
- When modifying existing files, maintain consistency with current code style
|
|
||||||
- Use git for version control (as seen in .gitignore)
|
|
||||||
- Prefer to work with the project's existing toolchain (Bundler, etc.)
|
|
||||||
|
|
||||||
## Behavior
|
|
||||||
- When asked to make changes, first understand the context by examining relevant files
|
|
||||||
- When creating new files, ensure they follow project conventions
|
|
||||||
- When modifying files, preserve existing code style and patterns
|
|
||||||
- When implementing new features, suggest appropriate file locations and naming conventions
|
|
||||||
- When debugging, suggest using the project's existing test suite and development tools
|
|
||||||
- When suggesting changes, provide clear explanations of why the change is beneficial
|
|
||||||
213
README.md
Normal file → Executable file
213
README.md
Normal file → Executable file
@@ -1,79 +1,212 @@
|
|||||||
# Aperonight - Party Booking Platform
|
# Aperonight - Event Booking Platform
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 🌃 Overview
|
## 🌃 Overview
|
||||||
|
|
||||||
**Aperonight** is a two-sided marketplace connecting party-goers with nightlife promoters in Paris. The platform allows:
|
**Aperonight** is a comprehensive ticket selling system that connects event-goers with event organizers. The platform provides a complete solution for event booking, payment processing, and ticket management.
|
||||||
|
|
||||||
- **Customers** to discover/book tickets for upcoming parties
|
|
||||||
- **Promoters** to create/manage events and validate tickets at venue entrances
|
|
||||||
|
|
||||||
## 🎯 Key Features
|
## 🎯 Key Features
|
||||||
|
|
||||||
### For Party-Goers
|
### For Event-Goers
|
||||||
✔ Browse upcoming parties with filters (date, location, music genre)
|
✅ **User Dashboard** - Personalized metrics showing booked events, upcoming events, and event statistics
|
||||||
✔ Book tickets with multiple bundle options (VIP, group passes, etc.)
|
✅ **Event Discovery** - Browse upcoming events with detailed information and venue details
|
||||||
✔ Secure payment processing (credit cards, Apple/Google Pay)
|
✅ **Secure Booking** - Multiple ticket types per event with quantity selection
|
||||||
✔ Mobile-friendly e-tickets with QR codes
|
✅ **Stripe Integration** - Secure payment processing with credit/debit cards
|
||||||
|
✅ **PDF Tickets** - Automatically generated tickets with unique QR codes for each purchase
|
||||||
|
✅ **Download System** - Instant PDF ticket downloads after successful payment
|
||||||
|
|
||||||
### For Promoters
|
### For Event Organizers
|
||||||
✔ Event creation dashboard with ticket type customization
|
✅ **Event Management** - Create and manage events with detailed information
|
||||||
✔ Real-time ticket validation via mobile scanning
|
✅ **Ticket Type Configuration** - Set up multiple ticket types with different pricing
|
||||||
✔ Sales analytics and attendee tracking
|
✅ **Sales Tracking** - Monitor ticket sales and availability
|
||||||
✔ Automatic aggregation of events from partner platforms
|
✅ **User Authentication** - Secure user registration and login system
|
||||||
|
|
||||||
|
### Technical Implementation
|
||||||
|
✅ **Payment Processing** - Full Stripe Checkout integration with session management
|
||||||
|
✅ **PDF Generation** - Custom PDF tickets with QR codes using Prawn library
|
||||||
|
✅ **Responsive Design** - Mobile-friendly interface with Tailwind CSS
|
||||||
|
✅ **Database Relations** - Proper user-event-ticket relationships
|
||||||
|
|
||||||
## 🛠 Technical Stack
|
## 🛠 Technical Stack
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
- **Ruby on Rails 7** (API mode)
|
- **Ruby on Rails 8.0+** with Hotwire for reactive UI
|
||||||
- **MariaDB** database
|
- **MySQL** database with comprehensive migrations
|
||||||
<!--- **Redis** for caching/background jobs-->
|
- **Devise** for user authentication and session management
|
||||||
- **ActiveJob** for background processing
|
- **Kaminari** for pagination
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
- **Hotwire (Turbo + Stimulus)** for reactive UI
|
- **Hotwire (Turbo + Stimulus)** for interactive JavaScript behavior
|
||||||
- **Tailwind CSS** for styling
|
- **Tailwind CSS** for responsive styling and modern UI
|
||||||
- **React Native** for promoter mobile app (ticket scanning)
|
- **JavaScript Controllers** for cart management and checkout flow
|
||||||
|
|
||||||
### Key Integrations
|
### Key Integrations
|
||||||
- **Stripe Connect** for payments & promoter payouts
|
- **Stripe** for secure payment processing and checkout sessions
|
||||||
- **Shogun/Bizouk/Weezevent APIs** for event aggregation
|
- **Prawn & Prawn-QRCode** for PDF ticket generation
|
||||||
<!--- **Twilio** for SMS ticket delivery-->
|
- **RQRCode** for unique QR code generation per ticket
|
||||||
<!--- **AWS S3** for media storage-->
|
|
||||||
|
|
||||||
## 📊 Database Schema (Simplified)
|
## 📊 Database Schema
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
erDiagram
|
erDiagram
|
||||||
USER ||--o{ BOOKING : makes
|
USER ||--o{ EVENT : creates
|
||||||
|
USER ||--o{ TICKET : purchases
|
||||||
USER {
|
USER {
|
||||||
integer id
|
integer id
|
||||||
string email
|
string email
|
||||||
string encrypted_password
|
string encrypted_password
|
||||||
|
string first_name
|
||||||
|
string last_name
|
||||||
}
|
}
|
||||||
PROMOTER ||--o{ PARTY : creates
|
EVENT ||--o{ TICKET_TYPE : has
|
||||||
PROMOTER {
|
EVENT {
|
||||||
integer id
|
|
||||||
string stripe_account_id
|
|
||||||
}
|
|
||||||
PARTY ||--o{ TICKET_TYPE : has
|
|
||||||
PARTY {
|
|
||||||
integer id
|
integer id
|
||||||
|
integer user_id
|
||||||
|
string name
|
||||||
|
string slug
|
||||||
|
text description
|
||||||
|
string venue_name
|
||||||
|
string venue_address
|
||||||
|
decimal latitude
|
||||||
|
decimal longitude
|
||||||
datetime start_time
|
datetime start_time
|
||||||
}
|
datetime end_time
|
||||||
BOOKING ||--o{ TICKET : generates
|
string state
|
||||||
BOOKING {
|
boolean featured
|
||||||
integer id
|
string image
|
||||||
decimal total_price
|
|
||||||
}
|
}
|
||||||
TICKET_TYPE ||--o{ TICKET : defines
|
TICKET_TYPE ||--o{ TICKET : defines
|
||||||
TICKET_TYPE {
|
TICKET_TYPE {
|
||||||
integer id
|
integer id
|
||||||
|
integer event_id
|
||||||
string name
|
string name
|
||||||
|
text description
|
||||||
|
integer price_cents
|
||||||
|
integer quantity
|
||||||
|
datetime sale_start_at
|
||||||
|
datetime sale_end_at
|
||||||
|
boolean requires_id
|
||||||
|
integer minimum_age
|
||||||
}
|
}
|
||||||
TICKET {
|
TICKET {
|
||||||
integer id
|
integer id
|
||||||
|
integer user_id
|
||||||
|
integer ticket_type_id
|
||||||
string qr_code
|
string qr_code
|
||||||
|
integer price_cents
|
||||||
|
string status
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Ruby 3.4+
|
||||||
|
- Rails 8.0+
|
||||||
|
- MySQL/MariaDB
|
||||||
|
- Node.js 18+ (for asset compilation)
|
||||||
|
- Stripe account (for payment processing)
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. **Clone the repository**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/aperonight.git
|
||||||
|
cd aperonight
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install dependencies**
|
||||||
|
```bash
|
||||||
|
bundle install
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Database setup**
|
||||||
|
```bash
|
||||||
|
rails db:create
|
||||||
|
rails db:migrate
|
||||||
|
rails db:seed
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Configure environment variables**
|
||||||
|
Create a `.env` file or configure Rails credentials:
|
||||||
|
```bash
|
||||||
|
# Stripe configuration
|
||||||
|
STRIPE_PUBLISHABLE_KEY=pk_test_your_key_here
|
||||||
|
STRIPE_SECRET_KEY=sk_test_your_key_here
|
||||||
|
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here
|
||||||
|
|
||||||
|
# Database configuration (if not using defaults)
|
||||||
|
DATABASE_URL=mysql2://username:password@localhost/aperonight_development
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Start the development server**
|
||||||
|
```bash
|
||||||
|
rails server
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit `http://localhost:3000` to see the application running.
|
||||||
|
|
||||||
|
## 💳 Payment Configuration
|
||||||
|
|
||||||
|
### Setting up Stripe
|
||||||
|
|
||||||
|
1. Create a Stripe account at [stripe.com](https://stripe.com)
|
||||||
|
2. Get your API keys from the Stripe Dashboard
|
||||||
|
3. Add your keys to the Rails credentials or environment variables
|
||||||
|
4. Configure webhook endpoints for payment confirmations:
|
||||||
|
- Endpoint URL: `your-domain.com/stripe/webhooks`
|
||||||
|
- Events: `checkout.session.completed`, `payment_intent.succeeded`
|
||||||
|
|
||||||
|
## 🎫 Core Functionality
|
||||||
|
|
||||||
|
### User Flow
|
||||||
|
1. **Registration/Login** - Users create accounts or sign in
|
||||||
|
2. **Event Discovery** - Browse events from the homepage or events page
|
||||||
|
3. **Ticket Selection** - Choose ticket types and quantities
|
||||||
|
4. **Checkout** - Secure payment through Stripe Checkout
|
||||||
|
5. **Ticket Generation** - Automatic PDF ticket generation with QR codes
|
||||||
|
6. **Download** - Instant ticket download after payment
|
||||||
|
|
||||||
|
### Event Management
|
||||||
|
1. **Event Creation** - Create events with full details and images
|
||||||
|
2. **Ticket Types** - Configure multiple ticket types with pricing
|
||||||
|
3. **Sales Tracking** - Monitor ticket sales through the dashboard
|
||||||
|
|
||||||
|
### Dashboard Features
|
||||||
|
- **Personal Metrics** - View booked events and upcoming events
|
||||||
|
- **Event Sections** - Today's events, tomorrow's events, and upcoming events
|
||||||
|
- **Quick Actions** - Easy navigation to event discovery and booking
|
||||||
|
|
||||||
|
## 🔧 Development
|
||||||
|
|
||||||
|
### Key Files Structure
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── controllers/
|
||||||
|
│ ├── events_controller.rb # Event listing, booking, checkout
|
||||||
|
│ └── pages_controller.rb # Dashboard and static pages
|
||||||
|
├── models/
|
||||||
|
│ ├── user.rb # User authentication with Devise
|
||||||
|
│ ├── event.rb # Event management and states
|
||||||
|
│ ├── ticket_type.rb # Ticket configuration
|
||||||
|
│ └── ticket.rb # Ticket generation with QR codes
|
||||||
|
├── services/
|
||||||
|
│ └── ticket_pdf_generator.rb # PDF ticket generation service
|
||||||
|
└── views/
|
||||||
|
├── events/
|
||||||
|
│ ├── show.html.erb # Event details and booking
|
||||||
|
│ └── payment_success.html.erb # Post-purchase confirmation
|
||||||
|
└── pages/
|
||||||
|
└── dashboard.html.erb # User dashboard with metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Routes
|
||||||
|
- `GET /` - Homepage
|
||||||
|
- `GET /dashboard` - User dashboard (authenticated)
|
||||||
|
- `GET /events` - Event listings
|
||||||
|
- `GET /events/:slug.:id` - Event details and booking
|
||||||
|
- `POST /events/:slug.:id/checkout` - Stripe checkout initiation
|
||||||
|
- `GET /payment/success` - Payment confirmation
|
||||||
|
- `GET /tickets/:ticket_id/download` - PDF ticket download
|
||||||
|
|||||||
124
REFACTORING_SUMMARY.md
Normal file
124
REFACTORING_SUMMARY.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# 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
|
||||||
0
app/assets/builds/.keep
Normal file → Executable file
0
app/assets/builds/.keep
Normal file → Executable file
0
app/assets/images/.keep
Normal file → Executable file
0
app/assets/images/.keep
Normal file → Executable file
47
app/assets/stylesheets/application.postcss.css
Normal file → Executable file
47
app/assets/stylesheets/application.postcss.css
Normal file → Executable file
@@ -9,41 +9,20 @@
|
|||||||
/* Import components */
|
/* Import components */
|
||||||
@import "components/hero";
|
@import "components/hero";
|
||||||
@import "components/flash";
|
@import "components/flash";
|
||||||
@import "components/footer";
|
@import "components/event-finder";
|
||||||
@import "components/party-finder";
|
|
||||||
|
|
||||||
/* Base styles */
|
/* Import pages */
|
||||||
body {
|
@import "pages/home";
|
||||||
font-family: var(--font-sans);
|
|
||||||
line-height: 1.6;
|
/* QR Code Styles */
|
||||||
color: var(--color-neutral-900);
|
.qr-code-container {
|
||||||
background: var(--color-neutral-50);
|
@apply flex items-center justify-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* App wrapper */
|
.qr-code-container svg {
|
||||||
.app-wrapper {
|
max-width: 100% !important;
|
||||||
min-height: 100vh;
|
max-height: 100% !important;
|
||||||
display: flex;
|
width: 208px !important;
|
||||||
flex-direction: column;
|
height: 208px !important;
|
||||||
}
|
display: block !important;
|
||||||
|
|
||||||
/* Main content */
|
|
||||||
main {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footer */
|
|
||||||
.footer {
|
|
||||||
background: var(--color-neutral-800);
|
|
||||||
color: var(--color-neutral-300);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Flash messages */
|
|
||||||
.flash {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Yield content */
|
|
||||||
.yield {
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|||||||
4
app/assets/stylesheets/components/party-finder.css → app/assets/stylesheets/components/event-finder.css
Normal file → Executable file
4
app/assets/stylesheets/components/party-finder.css → app/assets/stylesheets/components/event-finder.css
Normal file → Executable file
@@ -1,4 +1,4 @@
|
|||||||
.party-finder {
|
.event-finder {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: var(--radius-2xl);
|
border-radius: var(--radius-2xl);
|
||||||
box-shadow: var(--shadow-2xl);
|
box-shadow: var(--shadow-2xl);
|
||||||
@@ -176,7 +176,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.party-finder {
|
.event-finder {
|
||||||
margin: var(--space-8) auto;
|
margin: var(--space-8) auto;
|
||||||
padding: var(--space-6);
|
padding: var(--space-6);
|
||||||
}
|
}
|
||||||
79
app/assets/stylesheets/components/flash.css
Normal file → Executable file
79
app/assets/stylesheets/components/flash.css
Normal file → Executable file
@@ -1,39 +1,70 @@
|
|||||||
/* Flash Messages - Theme Integration */
|
.notification {
|
||||||
.flash-message {
|
font-family: var(--font-sans, 'Plus Jakarta Sans', sans-serif);
|
||||||
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-4;
|
box-shadow: var(--shadow-lg);
|
||||||
|
border: 1px solid;
|
||||||
|
transition: all var(--duration-normal, 0.3s) ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base styles for all flash messages */
|
.notification-icon {
|
||||||
.flash-message .flex {
|
min-width: 20px;
|
||||||
@apply rounded-md p-4 border shadow-md;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Success message styles */
|
.notification-icon i {
|
||||||
.flash-message-success {
|
color: currentColor !important;
|
||||||
@apply bg-green-50 border-green-100 text-green-800;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Error message styles */
|
.notification-success {
|
||||||
.flash-message-error {
|
background: var(--color-success-light, #dcfce7);
|
||||||
@apply bg-red-50 border-red-100 text-red-800;
|
color: var(--color-success-dark, #15803d);
|
||||||
|
border-color: var(--color-success, #22c55e);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Warning message styles */
|
.notification-warning {
|
||||||
.flash-message-warning {
|
background: var(--color-warning-light, #fef3c7);
|
||||||
@apply bg-yellow-50 border-yellow-100 text-yellow-800;
|
color: var(--color-warning-dark, #92400e);
|
||||||
|
border-color: var(--color-warning, #f59e0b);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Info message styles */
|
.notification-error {
|
||||||
.flash-message-info {
|
background: var(--color-danger-light, #fecaca);
|
||||||
@apply bg-blue-50 border-blue-100 text-blue-800;
|
color: var(--color-danger-dark, #dc2626);
|
||||||
|
border-color: var(--color-danger, #ef4444);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Notice message styles */
|
.notification-info {
|
||||||
.flash-message-notice {
|
background: var(--color-primary-50, #f0f9ff);
|
||||||
@apply bg-purple-50 border-purple-100 text-purple-800;
|
color: var(--color-primary-800, #1e40af);
|
||||||
|
border-color: var(--color-primary-200, #bfdbfe);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Alert message styles */
|
/* Fallback colors if CSS variables are not available */
|
||||||
.flash-message-alert {
|
.notification-success {
|
||||||
@apply bg-red-50 border-red-100 text-red-800;
|
background: #dcfce7;
|
||||||
|
color: #15803d;
|
||||||
|
border-color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-warning {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
border-color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-error {
|
||||||
|
background: #fecaca;
|
||||||
|
color: #dc2626;
|
||||||
|
border-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-info {
|
||||||
|
background: #f0f9ff;
|
||||||
|
color: #1e40af;
|
||||||
|
border-color: #bfdbfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation for fade out */
|
||||||
|
.flash-messages-container .notification.opacity-0 {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
}
|
}
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
|
|
||||||
.footer {
|
|
||||||
background: var(--color-neutral-800);
|
|
||||||
color: var(--color-neutral-300);
|
|
||||||
padding: var(--space-8) 0 var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-content {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--space-6);
|
|
||||||
margin-bottom: var(--space-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.footer-content {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.footer-content {
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-section h3 {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: var(--space-3);
|
|
||||||
color: white;
|
|
||||||
font-size: var(--text-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links li {
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links a {
|
|
||||||
color: var(--color-neutral-400);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color var(--duration-normal);
|
|
||||||
font-size: var(--text-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links a:hover {
|
|
||||||
color: var(--color-accent-400);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-bottom {
|
|
||||||
border-top: 1px solid var(--color-neutral-700);
|
|
||||||
padding-top: var(--space-4);
|
|
||||||
text-align: center;
|
|
||||||
color: var(--color-neutral-400);
|
|
||||||
font-size: var(--text-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.hero {
|
|
||||||
padding: var(--space-8) 0 var(--space-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-group {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-stats {
|
|
||||||
gap: var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.features-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
0
app/assets/stylesheets/components/header.css
Executable file
0
app/assets/stylesheets/components/header.css
Executable file
0
app/assets/stylesheets/components/hero.css
Normal file → Executable file
0
app/assets/stylesheets/components/hero.css
Normal file → Executable file
90
app/assets/stylesheets/pages/events.css
Executable file
90
app/assets/stylesheets/pages/events.css
Executable file
@@ -0,0 +1,90 @@
|
|||||||
|
/* Events page specific styles */
|
||||||
|
|
||||||
|
.events-page {
|
||||||
|
background: linear-gradient(135deg, var(--color-neutral-50) 0%, var(--color-neutral-100) 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-page .breadcrumb {
|
||||||
|
padding: var(--space-4) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-page .event-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: var(--radius-2xl);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
transition: all var(--duration-slow) var(--ease-out);
|
||||||
|
border: 1px solid var(--color-neutral-200);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-page .event-card:hover {
|
||||||
|
transform: translateY(-8px) scale(1.02);
|
||||||
|
box-shadow: var(--shadow-2xl);
|
||||||
|
border-color: var(--color-primary-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-page .event-date-badge {
|
||||||
|
background: linear-gradient(135deg, var(--color-primary-100) 0%, var(--color-accent-100) 100%);
|
||||||
|
color: var(--color-primary-800);
|
||||||
|
font-weight: 700;
|
||||||
|
padding: var(--space-1) var(--space-3);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-page .price-highlight {
|
||||||
|
color: var(--color-primary-600);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-page .pagination {
|
||||||
|
margin-top: var(--space-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-page .pagination .page,
|
||||||
|
.events-page .pagination .next,
|
||||||
|
.events-page .pagination .last,
|
||||||
|
.events-page .pagination .prev,
|
||||||
|
.events-page .pagination .first {
|
||||||
|
padding: var(--space-2) var(--space-4);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
margin: 0 var(--space-1);
|
||||||
|
transition: all var(--duration-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-page .pagination .page:hover,
|
||||||
|
.events-page .pagination .next:hover,
|
||||||
|
.events-page .pagination .last:hover,
|
||||||
|
.events-page .pagination .prev:hover,
|
||||||
|
.events-page .pagination .first:hover {
|
||||||
|
background: var(--color-primary-100);
|
||||||
|
color: var(--color-primary-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-page .pagination .current {
|
||||||
|
background: var(--color-primary-600);
|
||||||
|
color: white;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-page .no-events-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: var(--radius-2xl);
|
||||||
|
padding: var(--space-12);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
text-align: center;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.events-page .event-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-page .no-events-card {
|
||||||
|
padding: var(--space-8);
|
||||||
|
}
|
||||||
|
}
|
||||||
171
app/assets/stylesheets/pages/home.css
Executable file
171
app/assets/stylesheets/pages/home.css
Executable file
@@ -0,0 +1,171 @@
|
|||||||
|
/* Updated Featured Events Grid - 3 Cards Side by Side */
|
||||||
|
.featured-events-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-8);
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.featured-events-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.featured-events-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-event-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
transition: all var(--duration-slow) var(--ease-out);
|
||||||
|
border: 1px solid var(--color-neutral-200);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-event-card:hover {
|
||||||
|
transform: translateY(-8px) scale(1.02);
|
||||||
|
box-shadow: var(--shadow-2xl);
|
||||||
|
border-color: var(--color-primary-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-event-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 240px;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform var(--duration-slow) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-event-card:hover .featured-event-image {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-event-content {
|
||||||
|
padding: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-event-badges {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-event-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
color: var(--color-neutral-900);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-event-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-event-meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
color: var(--color-neutral-600);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-event-description {
|
||||||
|
color: var(--color-neutral-700);
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-event-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-event-price {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--color-primary-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.featured-event-image {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-event-content {
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced animations */
|
||||||
|
.animate-slideInLeft {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30px);
|
||||||
|
transition: all 0.5s var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slideInLeft.visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slideInRight {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
transition: all 0.5s var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slideInRight.visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Added missing animation for fadeInUp */
|
||||||
|
.animate-fadeInUp {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
transition: all 0.5s var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fadeInUp.visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Feature Stats Styling */
|
||||||
|
.feature-stat {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-primary-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--color-neutral-600);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
141
app/assets/stylesheets/pdf.css
Normal file
141
app/assets/stylesheets/pdf.css
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
141
app/assets/stylesheets/pdf.scss
Normal file
141
app/assets/stylesheets/pdf.scss
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/* 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
app/assets/stylesheets/theme.css
Normal file → Executable file
5
app/assets/stylesheets/theme.css
Normal file → Executable file
@@ -100,11 +100,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Base styles */
|
/* Base styles */
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
|
|||||||
100
app/controllers/api/v1/events_controller.rb
Executable file
100
app/controllers/api/v1/events_controller.rb
Executable file
@@ -0,0 +1,100 @@
|
|||||||
|
# Contrôleur API pour la gestion des ressources d'événements
|
||||||
|
# Fournit des points de terminaison RESTful pour les opérations CRUD sur le modèle Event
|
||||||
|
|
||||||
|
module Api
|
||||||
|
module V1
|
||||||
|
class EventsController < ApiController
|
||||||
|
# Skip API key authentication for store_cart action (used by frontend forms)
|
||||||
|
skip_before_action :authenticate_api_key, only: [ :store_cart ]
|
||||||
|
|
||||||
|
# Charge l'évén avant certaines actions pour réduire les duplications
|
||||||
|
before_action :set_event, only: [ :show, :update, :destroy, :store_cart ]
|
||||||
|
|
||||||
|
# GET /api/v1/events
|
||||||
|
# Récupère tous les événements triés par date de création (du plus récent au plus ancien)
|
||||||
|
def index
|
||||||
|
@events = Event.all.order(created_at: :desc)
|
||||||
|
render json: @events, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /api/v1/events/:id
|
||||||
|
# Récupère un seul événement par son ID
|
||||||
|
# Retourne 404 si l'événement n'est pas trouvé
|
||||||
|
def show
|
||||||
|
render json: @event, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /api/v1/events
|
||||||
|
# Crée un nouvel événement avec les attributs fournis
|
||||||
|
# Retourne 201 Created en cas de succès avec les données de l'événement
|
||||||
|
# Retourne 422 Unprocessable Entity avec les messages d'erreur en cas d'échec
|
||||||
|
def create
|
||||||
|
@event = Event.new(event_params)
|
||||||
|
if @event.save
|
||||||
|
render json: @event, status: :created
|
||||||
|
else
|
||||||
|
render json: { errors: @event.errors.full_messages }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# PATCH/PUT /api/v1/events/:id
|
||||||
|
# Met à jour un événement existant avec les attributs fournis
|
||||||
|
# Retourne 200 OK avec les données mises à jour en cas de succès
|
||||||
|
# Retourne 422 Unprocessable Entity avec les messages d'erreur en cas d'échec
|
||||||
|
def update
|
||||||
|
if @event.update(event_params)
|
||||||
|
render json: @event, status: :ok
|
||||||
|
else
|
||||||
|
render json: { errors: @event.errors.full_messages }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# DELETE /api/v1/events/:id
|
||||||
|
# Supprime définitivement un événement
|
||||||
|
# Retourne 204 No Content en cas de succès
|
||||||
|
def destroy
|
||||||
|
@event.destroy
|
||||||
|
head :no_content
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /api/v1/events/:id/store_cart
|
||||||
|
# Store cart data in session (AJAX endpoint)
|
||||||
|
def store_cart
|
||||||
|
cart_data = params[:cart] || {}
|
||||||
|
session[:pending_cart] = cart_data
|
||||||
|
session[:event_id] = @event.id
|
||||||
|
|
||||||
|
render json: { status: "success", message: "Cart stored successfully" }
|
||||||
|
rescue => e
|
||||||
|
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
||||||
|
Rails.logger.error "Error storing cart: #{error_message}"
|
||||||
|
render json: { status: "error", message: "Failed to store cart" }, status: 500
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Trouve un événement par son ID ou retourne 404 Introuvable
|
||||||
|
# Utilisé comme before_action pour les actions show, update et destroy
|
||||||
|
def set_event
|
||||||
|
@event = Event.find(params[:id])
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
render json: { error: "Événement non trouvé" }, status: :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
# Paramètres forts pour la création et la mise à jour des événements
|
||||||
|
# Liste blanche des attributs autorisés pour éviter les vulnérabilités de mass assignment
|
||||||
|
def event_params
|
||||||
|
params.require(:event).permit(
|
||||||
|
:name,
|
||||||
|
:description,
|
||||||
|
:state,
|
||||||
|
:venue_name,
|
||||||
|
:venue_address,
|
||||||
|
:latitude,
|
||||||
|
:longitude,
|
||||||
|
:featured
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
279
app/controllers/api/v1/orders_controller.rb
Normal file
279
app/controllers/api/v1/orders_controller.rb
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
# API controller for order management
|
||||||
|
# Provides RESTful endpoints for order operations
|
||||||
|
|
||||||
|
module Api
|
||||||
|
module V1
|
||||||
|
class OrdersController < ApiController
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :set_order, only: [ :show, :checkout, :retry_payment, :increment_payment_attempt ]
|
||||||
|
before_action :set_event, only: [ :new, :create ]
|
||||||
|
|
||||||
|
# GET /api/v1/orders/new
|
||||||
|
# Returns data needed for new order form
|
||||||
|
def new
|
||||||
|
cart_data = params[:cart_data] || session[:pending_cart] || {}
|
||||||
|
|
||||||
|
if cart_data.empty?
|
||||||
|
render json: { error: "Veuillez d'abord sélectionner vos billets sur la page de l'événement" }, status: :bad_request
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
tickets_needing_names = []
|
||||||
|
cart_data.each do |ticket_type_id, item|
|
||||||
|
ticket_type = @event.ticket_types.find_by(id: ticket_type_id)
|
||||||
|
next unless ticket_type
|
||||||
|
|
||||||
|
quantity = item["quantity"].to_i
|
||||||
|
next if quantity <= 0
|
||||||
|
|
||||||
|
quantity.times do |i|
|
||||||
|
tickets_needing_names << {
|
||||||
|
ticket_type_id: ticket_type.id,
|
||||||
|
ticket_type_name: ticket_type.name,
|
||||||
|
ticket_type_price: ticket_type.price_cents,
|
||||||
|
index: i
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: { tickets_needing_names: tickets_needing_names }, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /api/v1/orders
|
||||||
|
# Creates a new order with tickets
|
||||||
|
def create
|
||||||
|
cart_data = params[:cart_data] || session[:pending_cart] || {}
|
||||||
|
|
||||||
|
if cart_data.empty?
|
||||||
|
render json: { error: "Aucun billet sélectionné" }, status: :bad_request
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
success = false
|
||||||
|
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
@order = current_user.orders.create!(event: @event, status: "draft")
|
||||||
|
|
||||||
|
order_params[:tickets_attributes]&.each do |index, ticket_attrs|
|
||||||
|
next if ticket_attrs[:first_name].blank? || ticket_attrs[:last_name].blank?
|
||||||
|
|
||||||
|
ticket_type = @event.ticket_types.find(ticket_attrs[:ticket_type_id])
|
||||||
|
|
||||||
|
ticket = @order.tickets.build(
|
||||||
|
ticket_type: ticket_type,
|
||||||
|
first_name: ticket_attrs[:first_name],
|
||||||
|
last_name: ticket_attrs[:last_name],
|
||||||
|
status: "draft"
|
||||||
|
)
|
||||||
|
|
||||||
|
unless ticket.save
|
||||||
|
render json: { error: "Erreur lors de la création des billets: #{ticket.errors.full_messages.join(', ')}" }, status: :unprocessable_entity
|
||||||
|
raise ActiveRecord::Rollback
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if @order.tickets.present?
|
||||||
|
@order.calculate_total!
|
||||||
|
success = true
|
||||||
|
else
|
||||||
|
render json: { error: "Aucun billet valide créé" }, status: :unprocessable_entity
|
||||||
|
raise ActiveRecord::Rollback
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if success
|
||||||
|
session[:draft_order_id] = @order.id
|
||||||
|
session.delete(:pending_cart)
|
||||||
|
render json: { order: @order, redirect_to: checkout_order_path(@order) }, status: :created
|
||||||
|
end
|
||||||
|
rescue => e
|
||||||
|
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
||||||
|
render json: { error: "Une erreur est survenue: #{error_message}" }, status: :internal_server_error
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /api/v1/orders/:id
|
||||||
|
# Returns order summary
|
||||||
|
def show
|
||||||
|
tickets = @order.tickets.includes(:ticket_type)
|
||||||
|
render json: { order: @order, tickets: tickets }, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /api/v1/orders/:id/checkout
|
||||||
|
# Returns checkout data for an order
|
||||||
|
def checkout
|
||||||
|
if @order.expired?
|
||||||
|
@order.expire_if_overdue!
|
||||||
|
render json: { error: "Votre commande a expiré. Veuillez recommencer." }, status: :gone
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
tickets = @order.tickets.includes(:ticket_type)
|
||||||
|
total_amount = @order.total_amount_cents
|
||||||
|
expiring_soon = @order.expiring_soon?
|
||||||
|
|
||||||
|
checkout_session = nil
|
||||||
|
if Rails.application.config.stripe[:secret_key].present?
|
||||||
|
begin
|
||||||
|
checkout_session = create_stripe_session
|
||||||
|
rescue => e
|
||||||
|
error_message = e.message.present? ? e.message : "Erreur Stripe inconnue"
|
||||||
|
Rails.logger.error "Stripe checkout session creation failed: #{error_message}"
|
||||||
|
render json: { error: "Erreur lors de la création de la session de paiement" }, status: :internal_server_error
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
order: @order,
|
||||||
|
tickets: tickets,
|
||||||
|
total_amount: total_amount,
|
||||||
|
expiring_soon: expiring_soon,
|
||||||
|
checkout_session: checkout_session
|
||||||
|
}, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
# PATCH /api/v1/orders/:id/increment_payment_attempt
|
||||||
|
# Increments payment attempt counter
|
||||||
|
def increment_payment_attempt
|
||||||
|
@order.increment_payment_attempt!
|
||||||
|
render json: { success: true, attempts: @order.payment_attempts }, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /api/v1/orders/:id/retry_payment
|
||||||
|
# Allows retrying payment for failed orders
|
||||||
|
def retry_payment
|
||||||
|
unless @order.can_retry_payment?
|
||||||
|
render json: { error: "Cette commande ne peut plus être payée" }, status: :forbidden
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: { redirect_to: checkout_order_path(@order) }, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /api/v1/orders/payment_success
|
||||||
|
# Handles successful payment confirmation
|
||||||
|
def payment_success
|
||||||
|
session_id = params[:session_id]
|
||||||
|
|
||||||
|
stripe_configured = Rails.application.config.stripe[:secret_key].present?
|
||||||
|
Rails.logger.debug "Payment success - Stripe configured: #{stripe_configured}"
|
||||||
|
|
||||||
|
unless stripe_configured
|
||||||
|
render json: { error: "Le système de paiement n'est pas correctement configuré. Veuillez contacter l'administrateur." }, status: :service_unavailable
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
stripe_session = Stripe::Checkout::Session.retrieve(session_id)
|
||||||
|
|
||||||
|
if stripe_session.payment_status == "paid"
|
||||||
|
order_id = stripe_session.metadata["order_id"]
|
||||||
|
|
||||||
|
unless order_id.present?
|
||||||
|
render json: { error: "Informations de commande manquantes" }, status: :bad_request
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@order = current_user.orders.includes(tickets: :ticket_type).find(order_id)
|
||||||
|
@order.mark_as_paid!
|
||||||
|
|
||||||
|
begin
|
||||||
|
StripeInvoiceGenerationJob.perform_later(@order.id)
|
||||||
|
Rails.logger.info "Scheduled Stripe invoice generation for order #{@order.id}"
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "Failed to schedule invoice generation for order #{@order.id}: #{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
@order.tickets.each do |ticket|
|
||||||
|
begin
|
||||||
|
TicketMailer.purchase_confirmation(ticket).deliver_now
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "Failed to send confirmation email for ticket #{ticket.id}: #{e.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
session.delete(:pending_cart)
|
||||||
|
session.delete(:ticket_names)
|
||||||
|
session.delete(:draft_order_id)
|
||||||
|
|
||||||
|
render json: { order: @order, tickets: @order.tickets }, status: :ok
|
||||||
|
else
|
||||||
|
render json: { error: "Le paiement n'a pas été complété avec succès" }, status: :payment_required
|
||||||
|
end
|
||||||
|
rescue Stripe::StripeError => e
|
||||||
|
error_message = e.message.present? ? e.message : "Erreur Stripe inconnue"
|
||||||
|
render json: { error: "Erreur lors du traitement de votre confirmation de paiement : #{error_message}" }, status: :bad_request
|
||||||
|
rescue => e
|
||||||
|
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
||||||
|
Rails.logger.error "Payment success error: #{e.class} - #{error_message}"
|
||||||
|
render json: { error: "Une erreur inattendue s'est produite : #{error_message}" }, status: :internal_server_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /api/v1/orders/payment_cancel
|
||||||
|
# Handles payment cancellation
|
||||||
|
def payment_cancel
|
||||||
|
order_id = params[:order_id] || session[:draft_order_id]
|
||||||
|
|
||||||
|
if order_id.present?
|
||||||
|
order = current_user.orders.find_by(id: order_id, status: "draft")
|
||||||
|
|
||||||
|
if order&.can_retry_payment?
|
||||||
|
render json: { message: "Le paiement a été annulé. Vous pouvez réessayer.", redirect_to: checkout_order_path(order) }, status: :ok
|
||||||
|
else
|
||||||
|
session.delete(:draft_order_id)
|
||||||
|
render json: { message: "Le paiement a été annulé et votre commande a expiré." }, status: :gone
|
||||||
|
end
|
||||||
|
else
|
||||||
|
render json: { message: "Le paiement a été annulé" }, status: :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_order
|
||||||
|
@order = current_user.orders.includes(:tickets, :event).find(params[:id])
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
render json: { error: "Commande non trouvée" }, status: :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_event
|
||||||
|
@event = Event.includes(:ticket_types).find(params[:id])
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
render json: { error: "Événement non trouvé" }, status: :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
def order_params
|
||||||
|
params.permit(tickets_attributes: [ :ticket_type_id, :first_name, :last_name ])
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_stripe_session
|
||||||
|
line_items = @order.tickets.map do |ticket|
|
||||||
|
{
|
||||||
|
price_data: {
|
||||||
|
currency: "eur",
|
||||||
|
product_data: {
|
||||||
|
name: "#{@order.event.name} - #{ticket.ticket_type.name}",
|
||||||
|
description: ticket.ticket_type.description
|
||||||
|
},
|
||||||
|
unit_amount: ticket.price_cents
|
||||||
|
},
|
||||||
|
quantity: 1
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
Stripe::Checkout::Session.create(
|
||||||
|
payment_method_types: [ "card" ],
|
||||||
|
line_items: line_items,
|
||||||
|
mode: "payment",
|
||||||
|
success_url: order_payment_success_url + "?session_id={CHECKOUT_SESSION_ID}",
|
||||||
|
cancel_url: order_payment_cancel_url,
|
||||||
|
metadata: {
|
||||||
|
order_id: @order.id,
|
||||||
|
user_id: current_user.id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
# API controller for managing party resources
|
|
||||||
# Provides RESTful endpoints for CRUD operations on Party model
|
|
||||||
module Api
|
|
||||||
module V1
|
|
||||||
class PartiesController < ApiController
|
|
||||||
# Load party before specific actions to reduce duplication
|
|
||||||
before_action :set_party, only: [ :show, :update, :destroy ]
|
|
||||||
|
|
||||||
# GET /api/v1/parties
|
|
||||||
# Returns all parties sorted by creation date (newest first)
|
|
||||||
def index
|
|
||||||
@parties = Party.all.order(created_at: :desc)
|
|
||||||
render json: @parties, status: :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
# GET /api/v1/parties/:id
|
|
||||||
# Returns a single party by ID
|
|
||||||
# Returns 404 if party is not found
|
|
||||||
def show
|
|
||||||
render json: @party, status: :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
# POST /api/v1/parties
|
|
||||||
# Creates a new party with provided attributes
|
|
||||||
# Returns 201 Created on success with party data
|
|
||||||
# Returns 422 Unprocessable Entity with validation errors on failure
|
|
||||||
def create
|
|
||||||
@party = Party.new(party_params)
|
|
||||||
if @party.save
|
|
||||||
render json: @party, status: :created
|
|
||||||
else
|
|
||||||
render json: { errors: @party.errors.full_messages }, status: :unprocessable_entity
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# PATCH/PUT /api/v1/parties/:id
|
|
||||||
# Updates an existing party with provided attributes
|
|
||||||
# Returns 200 OK with updated party data on success
|
|
||||||
# Returns 422 Unprocessable Entity with validation errors on failure
|
|
||||||
def update
|
|
||||||
if @party.update(party_params)
|
|
||||||
render json: @party, status: :ok
|
|
||||||
else
|
|
||||||
render json: { errors: @party.errors.full_messages }, status: :unprocessable_entity
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# DELETE /api/v1/parties/:id
|
|
||||||
# Permanently deletes a party
|
|
||||||
# Returns 204 No Content on success
|
|
||||||
def destroy
|
|
||||||
@party.destroy
|
|
||||||
head :no_content
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# Finds a party by ID or returns 404 Not Found
|
|
||||||
# Used as before_action for show, update, and destroy actions
|
|
||||||
def set_party
|
|
||||||
@party = Party.find(params[:id])
|
|
||||||
rescue ActiveRecord::RecordNotFound
|
|
||||||
render json: { error: "Party not found" }, status: :not_found
|
|
||||||
end
|
|
||||||
|
|
||||||
# Strong parameters for party creation and updates
|
|
||||||
# Whitelists permitted attributes to prevent mass assignment vulnerabilities
|
|
||||||
def party_params
|
|
||||||
params.require(:party).permit(
|
|
||||||
:name,
|
|
||||||
:description,
|
|
||||||
:state,
|
|
||||||
:venue_name,
|
|
||||||
:venue_address,
|
|
||||||
:latitude,
|
|
||||||
:longitude,
|
|
||||||
:featured
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
6
app/controllers/api_controller.rb
Normal file → Executable file
6
app/controllers/api_controller.rb
Normal file → Executable file
@@ -16,8 +16,10 @@ class ApiController < ApplicationController
|
|||||||
# Extract API key from header or query parameter
|
# Extract API key from header or query parameter
|
||||||
api_key = request.headers["X-API-Key"] || params[:api_key]
|
api_key = request.headers["X-API-Key"] || params[:api_key]
|
||||||
|
|
||||||
# Validate against hardcoded key (in production, use environment variable)
|
# Validate against environment variable for security
|
||||||
unless api_key == "aperonight-api-key-2025"
|
expected_key = Rails.application.credentials.api_key || ENV["API_KEY"]
|
||||||
|
|
||||||
|
unless expected_key.present? && api_key == expected_key
|
||||||
render json: { error: "Unauthorized" }, status: :unauthorized
|
render json: { error: "Unauthorized" }, status: :unauthorized
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
44
app/controllers/application_controller.rb
Normal file → Executable file
44
app/controllers/application_controller.rb
Normal file → Executable file
@@ -14,4 +14,48 @@ class ApplicationController < ActionController::Base
|
|||||||
# - CSS nesting and :has() pseudo-class
|
# - CSS nesting and :has() pseudo-class
|
||||||
# allow_browser versions: :modern
|
# allow_browser versions: :modern
|
||||||
# allow_browser versions: { safari: 16.4, firefox: 121, ie: false }
|
# allow_browser versions: { safari: 16.4, firefox: 121, ie: false }
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
# Generate SEO-friendly path for an event
|
||||||
|
def seo_event_path(event)
|
||||||
|
year = event.start_time.year
|
||||||
|
month = format("%02d", event.start_time.month)
|
||||||
|
event_path(year: year, month: month, slug: event.slug)
|
||||||
|
end
|
||||||
|
helper_method :seo_event_path
|
||||||
|
|
||||||
|
# Generate SEO-friendly booking URL for an event
|
||||||
|
def seo_book_tickets_path(event)
|
||||||
|
year = event.start_time.year
|
||||||
|
month = format("%02d", event.start_time.month)
|
||||||
|
book_event_tickets_path(year: year, month: month, slug: event.slug)
|
||||||
|
end
|
||||||
|
helper_method :seo_book_tickets_path
|
||||||
|
|
||||||
|
# Generate SEO-friendly checkout URL for an event
|
||||||
|
def seo_checkout_path(event)
|
||||||
|
year = event.start_time.year
|
||||||
|
month = format("%02d", event.start_time.month)
|
||||||
|
event_checkout_path(year: year, month: month, slug: event.slug)
|
||||||
|
end
|
||||||
|
helper_method :seo_checkout_path
|
||||||
|
|
||||||
|
# Generate SEO-friendly ticket URL
|
||||||
|
def seo_ticket_path(ticket)
|
||||||
|
ticket_path(event_slug: ticket.event.slug, ticket_id: ticket.id)
|
||||||
|
end
|
||||||
|
helper_method :seo_ticket_path
|
||||||
|
|
||||||
|
# Generate SEO-friendly ticket view URL
|
||||||
|
def seo_ticket_view_path(ticket)
|
||||||
|
view_ticket_path(event_slug: ticket.event.slug, ticket_id: ticket.id)
|
||||||
|
end
|
||||||
|
helper_method :seo_ticket_view_path
|
||||||
|
|
||||||
|
# Generate SEO-friendly ticket download URL
|
||||||
|
def seo_ticket_download_path(ticket)
|
||||||
|
download_ticket_path(event_slug: ticket.event.slug, ticket_id: ticket.id)
|
||||||
|
end
|
||||||
|
helper_method :seo_ticket_download_path
|
||||||
end
|
end
|
||||||
|
|||||||
2
app/controllers/authentications/confirmations_controller.rb → app/controllers/auth/confirmations_controller.rb
Normal file → Executable file
2
app/controllers/authentications/confirmations_controller.rb → app/controllers/auth/confirmations_controller.rb
Normal file → Executable file
@@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Authentications::ConfirmationsController < Devise::ConfirmationsController
|
class Auth::ConfirmationsController < Devise::ConfirmationsController
|
||||||
# GET /resource/confirmation/new
|
# GET /resource/confirmation/new
|
||||||
# def new
|
# def new
|
||||||
# super
|
# super
|
||||||
2
app/controllers/authentications/omniauth_callbacks_controller.rb → app/controllers/auth/omniauth_callbacks_controller.rb
Normal file → Executable file
2
app/controllers/authentications/omniauth_callbacks_controller.rb → app/controllers/auth/omniauth_callbacks_controller.rb
Normal file → Executable file
@@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Authentications::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||||
# You should configure your model like this:
|
# You should configure your model like this:
|
||||||
# devise :omniauthable, omniauth_providers: [:twitter]
|
# devise :omniauthable, omniauth_providers: [:twitter]
|
||||||
|
|
||||||
2
app/controllers/authentications/passwords_controller.rb → app/controllers/auth/passwords_controller.rb
Normal file → Executable file
2
app/controllers/authentications/passwords_controller.rb → app/controllers/auth/passwords_controller.rb
Normal file → Executable file
@@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Authentications::PasswordsController < Devise::PasswordsController
|
class Auth::PasswordsController < Devise::PasswordsController
|
||||||
# GET /resource/password/new
|
# GET /resource/password/new
|
||||||
# def new
|
# def new
|
||||||
# super
|
# super
|
||||||
2
app/controllers/authentications/registrations_controller.rb → app/controllers/auth/registrations_controller.rb
Normal file → Executable file
2
app/controllers/authentications/registrations_controller.rb → app/controllers/auth/registrations_controller.rb
Normal file → Executable file
@@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Authentications::RegistrationsController < Devise::RegistrationsController
|
class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
before_action :configure_sign_up_params, only: [ :create ]
|
before_action :configure_sign_up_params, only: [ :create ]
|
||||||
before_action :configure_account_update_params, only: [ :update ]
|
before_action :configure_account_update_params, only: [ :update ]
|
||||||
|
|
||||||
2
app/controllers/authentications/sessions_controller.rb → app/controllers/auth/sessions_controller.rb
Normal file → Executable file
2
app/controllers/authentications/sessions_controller.rb → app/controllers/auth/sessions_controller.rb
Normal file → Executable file
@@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Authentications::SessionsController < Devise::SessionsController
|
class Auth::SessionsController < Devise::SessionsController
|
||||||
# before_action :configure_sign_in_params, only: [:create]
|
# before_action :configure_sign_in_params, only: [:create]
|
||||||
|
|
||||||
# GET /resource/sign_in
|
# GET /resource/sign_in
|
||||||
2
app/controllers/authentications/unlocks_controller.rb → app/controllers/auth/unlocks_controller.rb
Normal file → Executable file
2
app/controllers/authentications/unlocks_controller.rb → app/controllers/auth/unlocks_controller.rb
Normal file → Executable file
@@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Authentications::UnlocksController < Devise::UnlocksController
|
class Auth::UnlocksController < Devise::UnlocksController
|
||||||
# GET /resource/unlock/new
|
# GET /resource/unlock/new
|
||||||
# def new
|
# def new
|
||||||
# super
|
# super
|
||||||
92
app/controllers/booking/payments_controller.rb
Normal file
92
app/controllers/booking/payments_controller.rb
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Handle payment callbacks for booking workflow
|
||||||
|
class Booking::PaymentsController < ApplicationController
|
||||||
|
before_action :authenticate_user!
|
||||||
|
|
||||||
|
# Handle successful payment callback
|
||||||
|
def success
|
||||||
|
session_id = params[:session_id]
|
||||||
|
|
||||||
|
# Check if Stripe is properly configured
|
||||||
|
stripe_configured = Rails.application.config.stripe[:secret_key].present?
|
||||||
|
Rails.logger.debug "Payment success - Stripe configured: #{stripe_configured}"
|
||||||
|
|
||||||
|
unless stripe_configured
|
||||||
|
redirect_to root_path, alert: "Le système de paiement n'est pas correctement configuré. Veuillez contacter l'administrateur."
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
stripe_session = Stripe::Checkout::Session.retrieve(session_id)
|
||||||
|
|
||||||
|
if stripe_session.payment_status == "paid"
|
||||||
|
# Get order_id from session metadata
|
||||||
|
order_id = stripe_session.metadata["order_id"]
|
||||||
|
|
||||||
|
unless order_id.present?
|
||||||
|
redirect_to dashboard_path, alert: "Informations de commande manquantes"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# Find and update the order
|
||||||
|
@order = current_user.orders.includes(tickets: :ticket_type).find(order_id)
|
||||||
|
@order.mark_as_paid!
|
||||||
|
|
||||||
|
# Schedule Stripe invoice generation in background
|
||||||
|
begin
|
||||||
|
StripeInvoiceGenerationJob.perform_later(@order.id)
|
||||||
|
Rails.logger.info "Scheduled Stripe invoice generation for order #{@order.id}"
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "Failed to schedule invoice generation for order #{@order.id}: #{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# 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}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Clear session data
|
||||||
|
session.delete(:pending_cart)
|
||||||
|
session.delete(:ticket_names)
|
||||||
|
session.delete(:draft_order_id)
|
||||||
|
|
||||||
|
render "payment_success"
|
||||||
|
else
|
||||||
|
redirect_to dashboard_path, alert: "Le paiement n'a pas été complété avec succès"
|
||||||
|
end
|
||||||
|
rescue Stripe::StripeError => e
|
||||||
|
error_message = e.message.present? ? e.message : "Erreur Stripe inconnue"
|
||||||
|
redirect_to dashboard_path, alert: "Erreur lors du traitement de votre confirmation de paiement : #{error_message}"
|
||||||
|
rescue => e
|
||||||
|
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
||||||
|
Rails.logger.error "Payment success error: #{e.class} - #{error_message}"
|
||||||
|
redirect_to dashboard_path, alert: "Une erreur inattendue s'est produite : #{error_message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handle payment cancellation callback
|
||||||
|
def cancel
|
||||||
|
order_id = params[:order_id] || session[:draft_order_id]
|
||||||
|
|
||||||
|
if order_id.present?
|
||||||
|
order = current_user.orders.find_by(id: order_id, status: "draft")
|
||||||
|
|
||||||
|
if order&.can_retry_payment?
|
||||||
|
# Extract year and month from event start_time for SEO URL
|
||||||
|
year = order.event.start_time.year
|
||||||
|
month = format("%02d", order.event.start_time.month)
|
||||||
|
|
||||||
|
redirect_to event_checkout_path(year: year, month: month, slug: order.event.slug),
|
||||||
|
alert: "Le paiement a été annulé. Vous pouvez réessayer."
|
||||||
|
else
|
||||||
|
session.delete(:draft_order_id)
|
||||||
|
redirect_to root_path, alert: "Le paiement a été annulé et votre commande a expiré."
|
||||||
|
end
|
||||||
|
else
|
||||||
|
redirect_to root_path, alert: "Le paiement a été annulé"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
0
app/controllers/concerns/.keep
Normal file → Executable file
0
app/controllers/concerns/.keep
Normal file → Executable file
18
app/controllers/concerns/stripe_concern.rb
Normal file
18
app/controllers/concerns/stripe_concern.rb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
module StripeConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
# Check if Stripe is properly configured
|
||||||
|
def stripe_configured?
|
||||||
|
Rails.application.config.stripe[:secret_key].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Stripe is now initialized at application startup, so this method is no longer needed
|
||||||
|
# but kept for backward compatibility
|
||||||
|
def initialize_stripe
|
||||||
|
return false unless stripe_configured?
|
||||||
|
|
||||||
|
# Stripe is already initialized at application startup
|
||||||
|
Rails.logger.debug "Stripe already initialized at application startup"
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
59
app/controllers/events_controller.rb
Executable file
59
app/controllers/events_controller.rb
Executable file
@@ -0,0 +1,59 @@
|
|||||||
|
# Events controller - Public event listings and individual event display
|
||||||
|
#
|
||||||
|
# This controller manages public event browsing and displays individual events
|
||||||
|
# with their associated ticket types. No authentication required for public browsing.
|
||||||
|
class EventsController < ApplicationController
|
||||||
|
# No authentication required for public event viewing
|
||||||
|
before_action :authenticate_user!, only: []
|
||||||
|
before_action :set_event, only: [ :show ]
|
||||||
|
|
||||||
|
# Display paginated list of upcoming published events
|
||||||
|
#
|
||||||
|
# Shows events in published state, ordered by start time ascending
|
||||||
|
# Includes event owner information and supports Kaminari pagination
|
||||||
|
def index
|
||||||
|
@events = Event.includes(:user).upcoming.page(params[:page]).per(12)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Display individual event with ticket type information
|
||||||
|
#
|
||||||
|
# Shows complete event details including venue information,
|
||||||
|
# available ticket types, and allows users to add tickets to cart
|
||||||
|
def show
|
||||||
|
# Event is set by set_event callback with ticket types preloaded
|
||||||
|
# Template will display event details and ticket selection interface
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Find and set the current event with eager-loaded associations
|
||||||
|
# Supports both old slug-only format and new SEO-friendly year/month/slug format
|
||||||
|
# Loads event with ticket types to avoid N+1 queries
|
||||||
|
def set_event
|
||||||
|
if params[:year] && params[:month]
|
||||||
|
# New SEO-friendly format: /events/2024/07/summer-party
|
||||||
|
year = params[:year].to_i
|
||||||
|
month = params[:month].to_i
|
||||||
|
start_of_month = Date.new(year, month, 1).beginning_of_month
|
||||||
|
end_of_month = start_of_month.end_of_month
|
||||||
|
|
||||||
|
@event = Event.includes(:ticket_types)
|
||||||
|
.where(slug: params[:slug])
|
||||||
|
.where(start_time: start_of_month..end_of_month)
|
||||||
|
.first!
|
||||||
|
else
|
||||||
|
# Legacy format: /events/summer-party (for backward compatibility)
|
||||||
|
@event = Event.includes(:ticket_types).find_by!(slug: params[:slug])
|
||||||
|
end
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
redirect_to events_path, alert: "Événement non trouvé"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate SEO-friendly path for an event
|
||||||
|
def seo_event_path(event)
|
||||||
|
year = event.start_time.year
|
||||||
|
month = format("%02d", event.start_time.month)
|
||||||
|
event_path(year: year, month: month, slug: event.slug)
|
||||||
|
end
|
||||||
|
helper_method :seo_event_path
|
||||||
|
end
|
||||||
17
app/controllers/legacy_redirects_controller.rb
Normal file
17
app/controllers/legacy_redirects_controller.rb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Handle legacy URL redirects to new SEO-friendly URLs
|
||||||
|
class LegacyRedirectsController < ApplicationController
|
||||||
|
# Redirect old event URLs to new SEO-friendly format
|
||||||
|
# OLD: /events/summer-party-2024
|
||||||
|
# NEW: /events/2024/07/summer-party-2024
|
||||||
|
def event_redirect
|
||||||
|
event = Event.find_by(slug: params[:slug])
|
||||||
|
|
||||||
|
if event
|
||||||
|
year = event.start_time.year
|
||||||
|
month = format("%02d", event.start_time.month)
|
||||||
|
redirect_to event_path(year: year, month: month, slug: event.slug), status: :moved_permanently
|
||||||
|
else
|
||||||
|
redirect_to events_path, alert: "Événement non trouvé"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
228
app/controllers/orders_controller.rb
Normal file
228
app/controllers/orders_controller.rb
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
# Handle order management and checkout process with SEO-friendly URLs
|
||||||
|
#
|
||||||
|
# This controller manages the order lifecycle from checkout to payment completion
|
||||||
|
# Orders group multiple tickets together for better transaction management
|
||||||
|
class OrdersController < ApplicationController
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :set_event_from_seo_params, only: [:new, :create, :checkout]
|
||||||
|
before_action :set_order_from_id, only: [:show, :retry_payment, :increment_payment_attempt]
|
||||||
|
|
||||||
|
# Display new order form with name collection
|
||||||
|
#
|
||||||
|
# On this page user can see order summary and complete the tickets details
|
||||||
|
# (first name and last name) for each ticket ordered
|
||||||
|
def new
|
||||||
|
@cart_data = params[:cart_data] || session[:pending_cart] || {}
|
||||||
|
|
||||||
|
if @cart_data.empty?
|
||||||
|
redirect_to seo_event_path(@event), alert: "Veuillez d'abord sélectionner vos billets sur la page de l'événement"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# Build list of tickets requiring names
|
||||||
|
@tickets_needing_names = []
|
||||||
|
@cart_data.each do |ticket_type_id, item|
|
||||||
|
ticket_type = @event.ticket_types.find_by(id: ticket_type_id)
|
||||||
|
next unless ticket_type
|
||||||
|
|
||||||
|
quantity = item["quantity"].to_i
|
||||||
|
next if quantity <= 0
|
||||||
|
|
||||||
|
quantity.times do |i|
|
||||||
|
@tickets_needing_names << {
|
||||||
|
ticket_type_id: ticket_type.id,
|
||||||
|
ticket_type_name: ticket_type.name,
|
||||||
|
ticket_type_price: ticket_type.price_cents,
|
||||||
|
index: i
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create a new order with tickets
|
||||||
|
#
|
||||||
|
# Here a new order is created with associated tickets in draft state.
|
||||||
|
# When user is ready they can proceed to payment via the order checkout
|
||||||
|
def create
|
||||||
|
@cart_data = params[:cart_data] || session[:pending_cart] || {}
|
||||||
|
|
||||||
|
if @cart_data.empty?
|
||||||
|
redirect_to seo_event_path(@event), alert: "Aucun billet sélectionné"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
success = false
|
||||||
|
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
@order = current_user.orders.create!(event: @event, status: "draft")
|
||||||
|
|
||||||
|
order_params[:tickets_attributes]&.each do |index, ticket_attrs|
|
||||||
|
next if ticket_attrs[:first_name].blank? || ticket_attrs[:last_name].blank?
|
||||||
|
|
||||||
|
ticket_type = @event.ticket_types.find(ticket_attrs[:ticket_type_id])
|
||||||
|
|
||||||
|
ticket = @order.tickets.build(
|
||||||
|
ticket_type: ticket_type,
|
||||||
|
first_name: ticket_attrs[:first_name],
|
||||||
|
last_name: ticket_attrs[:last_name],
|
||||||
|
status: "draft"
|
||||||
|
)
|
||||||
|
|
||||||
|
unless ticket.save
|
||||||
|
flash[:alert] = "Erreur lors de la création des billets: #{ticket.errors.full_messages.join(', ')}"
|
||||||
|
raise ActiveRecord::Rollback
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if @order.tickets.present?
|
||||||
|
@order.calculate_total!
|
||||||
|
success = true
|
||||||
|
else
|
||||||
|
flash[:alert] = "Aucun billet valide créé"
|
||||||
|
raise ActiveRecord::Rollback
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handle redirects outside transaction
|
||||||
|
if success
|
||||||
|
session[:draft_order_id] = @order.id
|
||||||
|
session.delete(:pending_cart)
|
||||||
|
year = @event.start_time.year
|
||||||
|
month = format("%02d", @event.start_time.month)
|
||||||
|
redirect_to event_checkout_path(year: year, month: month, slug: @event.slug)
|
||||||
|
else
|
||||||
|
year = @event.start_time.year
|
||||||
|
month = format("%02d", @event.start_time.month)
|
||||||
|
redirect_to book_event_tickets_path(year: year, month: month, slug: @event.slug)
|
||||||
|
end
|
||||||
|
rescue => e
|
||||||
|
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
||||||
|
flash[:alert] = "Une erreur est survenue: #{error_message}"
|
||||||
|
year = @event.start_time.year
|
||||||
|
month = format("%02d", @event.start_time.month)
|
||||||
|
redirect_to book_event_tickets_path(year: year, month: month, slug: @event.slug)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Display order summary
|
||||||
|
def show
|
||||||
|
@tickets = @order.tickets.includes(:ticket_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Display payment page for an order (SEO-friendly checkout URL)
|
||||||
|
#
|
||||||
|
# Display a summary of all tickets in the order and permit user
|
||||||
|
# to proceed to payment via Stripe
|
||||||
|
def checkout
|
||||||
|
# Find order from session or create one
|
||||||
|
@order = current_user.orders.find_by(id: session[:draft_order_id], event: @event, status: "draft")
|
||||||
|
|
||||||
|
unless @order
|
||||||
|
redirect_to seo_event_path(@event), alert: "Aucune commande en attente trouvée"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handle expired orders
|
||||||
|
if @order.expired?
|
||||||
|
@order.expire_if_overdue!
|
||||||
|
return redirect_to seo_event_path(@event),
|
||||||
|
alert: "Votre commande a expiré. Veuillez recommencer."
|
||||||
|
end
|
||||||
|
|
||||||
|
@tickets = @order.tickets.includes(:ticket_type)
|
||||||
|
@total_amount = @order.total_amount_cents
|
||||||
|
@expiring_soon = @order.expiring_soon?
|
||||||
|
|
||||||
|
# Create Stripe checkout session if Stripe is configured
|
||||||
|
if Rails.application.config.stripe[:secret_key].present?
|
||||||
|
begin
|
||||||
|
@checkout_session = create_stripe_session
|
||||||
|
rescue => e
|
||||||
|
error_message = e.message.present? ? e.message : "Erreur Stripe inconnue"
|
||||||
|
Rails.logger.error "Stripe checkout session creation failed: #{error_message}"
|
||||||
|
flash[:alert] = "Erreur lors de la création de la session de paiement"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Increment payment attempt - called via AJAX when user clicks pay button
|
||||||
|
def increment_payment_attempt
|
||||||
|
@order.increment_payment_attempt!
|
||||||
|
render json: { success: true, attempts: @order.payment_attempts }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Allow users to retry payment for failed/cancelled payments
|
||||||
|
def retry_payment
|
||||||
|
unless @order.can_retry_payment?
|
||||||
|
redirect_to seo_event_path(@order.event),
|
||||||
|
alert: "Cette commande ne peut plus être payée"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
year = @order.event.start_time.year
|
||||||
|
month = format("%02d", @order.event.start_time.month)
|
||||||
|
redirect_to event_checkout_path(year: year, month: month, slug: @order.event.slug)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_event_from_seo_params
|
||||||
|
year = params[:year].to_i
|
||||||
|
month = params[:month].to_i
|
||||||
|
start_of_month = Date.new(year, month, 1).beginning_of_month
|
||||||
|
end_of_month = start_of_month.end_of_month
|
||||||
|
|
||||||
|
@event = Event.includes(:ticket_types)
|
||||||
|
.where(slug: params[:slug])
|
||||||
|
.where(start_time: start_of_month..end_of_month)
|
||||||
|
.first
|
||||||
|
|
||||||
|
return redirect_to events_path, alert: "Événement non trouvé" unless @event
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_order_from_id
|
||||||
|
@order = current_user.orders.includes(:tickets, :event).find(params[:order_id])
|
||||||
|
@event = @order.event
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
redirect_to root_path, alert: "Commande non trouvée"
|
||||||
|
end
|
||||||
|
|
||||||
|
def order_params
|
||||||
|
params.permit(tickets_attributes: [:ticket_type_id, :first_name, :last_name])
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_stripe_session
|
||||||
|
line_items = @order.tickets.map do |ticket|
|
||||||
|
{
|
||||||
|
price_data: {
|
||||||
|
currency: "eur",
|
||||||
|
product_data: {
|
||||||
|
name: "#{@order.event.name} - #{ticket.ticket_type.name}",
|
||||||
|
description: ticket.ticket_type.description
|
||||||
|
},
|
||||||
|
unit_amount: ticket.price_cents
|
||||||
|
},
|
||||||
|
quantity: 1
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
Stripe::Checkout::Session.create(
|
||||||
|
payment_method_types: ["card"],
|
||||||
|
line_items: line_items,
|
||||||
|
mode: "payment",
|
||||||
|
success_url: booking_payment_success_url + "?session_id={CHECKOUT_SESSION_ID}",
|
||||||
|
cancel_url: booking_payment_cancelled_url + "?order_id=#{@order.id}",
|
||||||
|
metadata: {
|
||||||
|
order_id: @order.id,
|
||||||
|
user_id: current_user.id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate SEO-friendly path for an event
|
||||||
|
def seo_event_path(event)
|
||||||
|
year = event.start_time.year
|
||||||
|
month = format("%02d", event.start_time.month)
|
||||||
|
event_path(year: year, month: month, slug: event.slug)
|
||||||
|
end
|
||||||
|
helper_method :seo_event_path
|
||||||
|
end
|
||||||
44
app/controllers/pages_controller.rb
Normal file → Executable file
44
app/controllers/pages_controller.rb
Normal file → Executable file
@@ -1,32 +1,50 @@
|
|||||||
# Controller for static pages and user dashboard
|
# Controller for static pages and user dashboard
|
||||||
# Handles basic page rendering and user-specific content
|
# Handles basic page rendering and user-specific content
|
||||||
class PagesController < ApplicationController
|
class PagesController < ApplicationController
|
||||||
# Skip authentication for public pages
|
|
||||||
# skip_before_action :authenticate_user!, only: [ :home ]
|
|
||||||
before_action :authenticate_user!, only: [ :dashboard ]
|
before_action :authenticate_user!, only: [ :dashboard ]
|
||||||
|
|
||||||
# Homepage showing featured parties
|
# Homepage showing featured events
|
||||||
|
#
|
||||||
|
# Display homepage with featured events and incoming ones
|
||||||
def home
|
def home
|
||||||
# @parties = Party.published.featured.limit(3)
|
@featured_events = Event.published.featured.limit(3)
|
||||||
# @parties = Party.where(state: :published).order(created_at: :desc)
|
|
||||||
|
|
||||||
if user_signed_in?
|
if user_signed_in?
|
||||||
return redirect_to(dashboard_path)
|
redirect_to(dashboard_path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# User dashboard showing personalized content
|
# User dashboard showing personalized content
|
||||||
# Accessible only to authenticated users
|
# Accessible only to authenticated users
|
||||||
def dashboard
|
def dashboard
|
||||||
@available_parties = Party.published.count
|
# Metrics for dashboard cards
|
||||||
@events_this_week = Party.published.where("start_time BETWEEN ? AND ?", Date.current.beginning_of_week, Date.current.end_of_week).count
|
@booked_events = current_user.orders.joins(tickets: { ticket_type: :event })
|
||||||
@today_parties = Party.published.where("DATE(start_time) = ?", Date.current).order(start_time: :asc)
|
.where(events: { state: :published })
|
||||||
@tomorrow_parties = Party.published.where("DATE(start_time) = ?", Date.current + 1).order(start_time: :asc)
|
.where(orders: { status: [ "paid", "completed" ] })
|
||||||
@other_parties = Party.published.upcoming.where.not("DATE(start_time) IN (?)", [Date.current, Date.current + 1]).order(start_time: :asc).page(params[:page])
|
.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)
|
||||||
|
|
||||||
|
# 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])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Events page showing all published parties with pagination
|
# Events page showing all published events with pagination
|
||||||
def events
|
def events
|
||||||
@parties = Party.published.order(created_at: :desc).page(params[:page])
|
@events = Event.published.order(created_at: :desc).page(params[:page])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
class PartiesController < ApplicationController
|
|
||||||
# Display all events
|
|
||||||
def index
|
|
||||||
@parties = Party.includes(:user).upcoming.page(params[:page]).per(1)
|
|
||||||
# @parties = Party.page(params[:page]).per(12)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Display desired event
|
|
||||||
def show
|
|
||||||
@party = Party.find(params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
# Handle checkout process
|
|
||||||
def checkout
|
|
||||||
@party = Party.find(params[:id])
|
|
||||||
cart_data = JSON.parse(params[:cart] || "{}")
|
|
||||||
|
|
||||||
if cart_data.empty?
|
|
||||||
redirect_to party_path(@party), alert: "Please select at least one ticket"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
# Create order items from cart
|
|
||||||
order_items = []
|
|
||||||
total_amount = 0
|
|
||||||
|
|
||||||
cart_data.each do |ticket_type_id, item|
|
|
||||||
ticket_type = @party.ticket_types.find_by(id: ticket_type_id)
|
|
||||||
next unless ticket_type
|
|
||||||
|
|
||||||
quantity = item["quantity"].to_i
|
|
||||||
next if quantity <= 0
|
|
||||||
|
|
||||||
# Check availability
|
|
||||||
available = ticket_type.quantity - ticket_type.tickets.count
|
|
||||||
if quantity > available
|
|
||||||
redirect_to party_path(@party), alert: "Not enough tickets available for #{ticket_type.name}"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
order_items << {
|
|
||||||
ticket_type: ticket_type,
|
|
||||||
quantity: quantity,
|
|
||||||
price_cents: ticket_type.price_cents
|
|
||||||
}
|
|
||||||
|
|
||||||
total_amount += ticket_type.price_cents * quantity
|
|
||||||
end
|
|
||||||
|
|
||||||
if order_items.empty?
|
|
||||||
redirect_to party_path(@party), alert: "Invalid order"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
# Here you would typically:
|
|
||||||
# 1. Create an Order record
|
|
||||||
# 2. Create Ticket records for each item
|
|
||||||
# 3. Redirect to payment processing
|
|
||||||
|
|
||||||
# For now, we'll just redirect with a success message
|
|
||||||
# In a real app, you'd redirect to a payment page
|
|
||||||
redirect_to party_path(@party), notice: "Order created successfully! Proceeding to payment..."
|
|
||||||
end
|
|
||||||
end
|
|
||||||
117
app/controllers/promoter/events_controller.rb
Normal file
117
app/controllers/promoter/events_controller.rb
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Promoter Events Controller
|
||||||
|
#
|
||||||
|
# Handles event management for promoters (event organizers)
|
||||||
|
# Allows promoters to create, edit, delete and manage their events
|
||||||
|
class Promoter::EventsController < ApplicationController
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :ensure_can_manage_events!
|
||||||
|
before_action :set_event, only: [ :show, :edit, :update, :destroy, :publish, :unpublish, :cancel, :mark_sold_out ]
|
||||||
|
|
||||||
|
# Display all events for the current promoter
|
||||||
|
def index
|
||||||
|
@events = current_user.events.order(created_at: :desc).page(params[:page]).per(10)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Display a specific event for the promoter
|
||||||
|
def show
|
||||||
|
# Event is set by set_event callback
|
||||||
|
end
|
||||||
|
|
||||||
|
# Show form to create a new event
|
||||||
|
def new
|
||||||
|
@event = current_user.events.build
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create a new event
|
||||||
|
def create
|
||||||
|
@event = current_user.events.build(event_params)
|
||||||
|
|
||||||
|
if @event.save
|
||||||
|
redirect_to promoter_event_path(@event), notice: "Event créé avec succès!"
|
||||||
|
else
|
||||||
|
render :new, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Show form to edit an existing event
|
||||||
|
def edit
|
||||||
|
# Event is set by set_event callback
|
||||||
|
end
|
||||||
|
|
||||||
|
# Update an existing event
|
||||||
|
def update
|
||||||
|
if @event.update(event_params)
|
||||||
|
redirect_to promoter_event_path(@event), notice: "Event mis à jour avec succès!"
|
||||||
|
else
|
||||||
|
render :edit, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Delete an event
|
||||||
|
def destroy
|
||||||
|
@event.destroy
|
||||||
|
redirect_to promoter_events_path, notice: "Event supprimé avec succès!"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Publish an event (make it visible to public)
|
||||||
|
def publish
|
||||||
|
if @event.draft?
|
||||||
|
@event.update(state: :published)
|
||||||
|
redirect_to promoter_event_path(@event), notice: "Event publié avec succès!"
|
||||||
|
else
|
||||||
|
redirect_to promoter_event_path(@event), alert: "Cet event ne peut pas être publié."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unpublish an event (make it draft)
|
||||||
|
def unpublish
|
||||||
|
if @event.published?
|
||||||
|
@event.update(state: :draft)
|
||||||
|
redirect_to promoter_event_path(@event), notice: "Event dépublié avec succès!"
|
||||||
|
else
|
||||||
|
redirect_to promoter_event_path(@event), alert: "Cet event ne peut pas être dépublié."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Cancel an event
|
||||||
|
def cancel
|
||||||
|
if @event.published?
|
||||||
|
@event.update(state: :canceled)
|
||||||
|
redirect_to promoter_event_path(@event), notice: "Event annulé avec succès!"
|
||||||
|
else
|
||||||
|
redirect_to promoter_event_path(@event), alert: "Cet event ne peut pas être annulé."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Mark event as sold out
|
||||||
|
def mark_sold_out
|
||||||
|
if @event.published?
|
||||||
|
@event.update(state: :sold_out)
|
||||||
|
redirect_to promoter_event_path(@event), notice: "Event marqué comme complet!"
|
||||||
|
else
|
||||||
|
redirect_to promoter_event_path(@event), alert: "Cet event ne peut pas être marqué comme complet."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ensure_can_manage_events!
|
||||||
|
unless current_user.can_manage_events?
|
||||||
|
redirect_to dashboard_path, alert: "Vous n'avez pas les permissions nécessaires pour gérer des événements."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_event
|
||||||
|
@event = current_user.events.find(params[:id])
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
redirect_to promoter_events_path, alert: "Event non trouvé ou vous n'avez pas accès à cet event."
|
||||||
|
end
|
||||||
|
|
||||||
|
def event_params
|
||||||
|
params.require(:event).permit(
|
||||||
|
:name, :slug, :description, :image,
|
||||||
|
:venue_name, :venue_address, :latitude, :longitude,
|
||||||
|
:start_time, :end_time, :featured
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
104
app/controllers/promoter/ticket_types_controller.rb
Normal file
104
app/controllers/promoter/ticket_types_controller.rb
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# Promoter Ticket Types Controller
|
||||||
|
#
|
||||||
|
# Handles ticket type (bundle) management for promoters
|
||||||
|
# Allows promoters to create, edit, delete and manage ticket types for their events
|
||||||
|
class Promoter::TicketTypesController < ApplicationController
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :ensure_can_manage_events!
|
||||||
|
before_action :set_event
|
||||||
|
before_action :set_ticket_type, only: [ :show, :edit, :update, :destroy ]
|
||||||
|
|
||||||
|
# Display all ticket types for an event
|
||||||
|
def index
|
||||||
|
@ticket_types = @event.ticket_types.order(:created_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Display a specific ticket type
|
||||||
|
def show
|
||||||
|
# Ticket type is set by set_ticket_type callback
|
||||||
|
end
|
||||||
|
|
||||||
|
# Show form to create a new ticket type
|
||||||
|
def new
|
||||||
|
@ticket_type = @event.ticket_types.build
|
||||||
|
# Set default values
|
||||||
|
@ticket_type.sale_start_at = Time.current
|
||||||
|
@ticket_type.sale_end_at = @event.start_time || 1.week.from_now
|
||||||
|
@ticket_type.requires_id = false
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create a new ticket type
|
||||||
|
def create
|
||||||
|
@ticket_type = @event.ticket_types.build(ticket_type_params)
|
||||||
|
|
||||||
|
if @ticket_type.save
|
||||||
|
redirect_to promoter_event_ticket_types_path(@event), notice: "Type de billet créé avec succès!"
|
||||||
|
else
|
||||||
|
render :new, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Show form to edit an existing ticket type
|
||||||
|
def edit
|
||||||
|
# Ticket type is set by set_ticket_type callback
|
||||||
|
end
|
||||||
|
|
||||||
|
# Update an existing ticket type
|
||||||
|
def update
|
||||||
|
if @ticket_type.update(ticket_type_params)
|
||||||
|
redirect_to promoter_event_ticket_type_path(@event, @ticket_type), notice: "Type de billet mis à jour avec succès!"
|
||||||
|
else
|
||||||
|
render :edit, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Delete a ticket type
|
||||||
|
def destroy
|
||||||
|
if @ticket_type.tickets.any?
|
||||||
|
redirect_to promoter_event_ticket_types_path(@event), alert: "Impossible de supprimer ce type de billet car des billets ont déjà été vendus."
|
||||||
|
else
|
||||||
|
@ticket_type.destroy
|
||||||
|
redirect_to promoter_event_ticket_types_path(@event), notice: "Type de billet supprimé avec succès!"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Duplicate an existing ticket type
|
||||||
|
def duplicate
|
||||||
|
original = @event.ticket_types.find(params[:id])
|
||||||
|
@ticket_type = original.dup
|
||||||
|
@ticket_type.name = "#{original.name} (Copie)"
|
||||||
|
|
||||||
|
if @ticket_type.save
|
||||||
|
redirect_to edit_promoter_event_ticket_type_path(@event, @ticket_type), notice: "Type de billet dupliqué avec succès!"
|
||||||
|
else
|
||||||
|
redirect_to promoter_event_ticket_types_path(@event), alert: "Erreur lors de la duplication."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ensure_can_manage_events!
|
||||||
|
unless current_user.can_manage_events?
|
||||||
|
redirect_to dashboard_path, alert: "Vous n'avez pas les permissions nécessaires pour gérer des événements."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_event
|
||||||
|
@event = current_user.events.find(params[:event_id])
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
redirect_to promoter_events_path, alert: "Event non trouvé ou vous n'avez pas accès à cet event."
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_ticket_type
|
||||||
|
@ticket_type = @event.ticket_types.find(params[:id])
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
redirect_to promoter_event_ticket_types_path(@event), alert: "Type de billet non trouvé."
|
||||||
|
end
|
||||||
|
|
||||||
|
def ticket_type_params
|
||||||
|
params.require(:ticket_type).permit(
|
||||||
|
:name, :description, :price_euros, :quantity,
|
||||||
|
:sale_start_at, :sale_end_at, :minimum_age, :requires_id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
127
app/controllers/tickets_controller.rb
Normal file
127
app/controllers/tickets_controller.rb
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# Tickets controller - handles ticket viewing and downloads with SEO-friendly URLs
|
||||||
|
#
|
||||||
|
# This controller manages individual ticket display and downloads
|
||||||
|
# Uses event-slug-ticket-id format for SEO-friendly URLs
|
||||||
|
class TicketsController < ApplicationController
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :set_ticket_from_seo_params, only: [:show, :view, :download, :retry_payment]
|
||||||
|
|
||||||
|
# Display ticket details
|
||||||
|
def show
|
||||||
|
@event = @ticket.event
|
||||||
|
end
|
||||||
|
|
||||||
|
# Display ticket in PDF-like format
|
||||||
|
def view
|
||||||
|
@event = @ticket.event
|
||||||
|
end
|
||||||
|
|
||||||
|
# Download PDF ticket - only accessible by ticket owner
|
||||||
|
# User must be authenticated to download ticket
|
||||||
|
def download
|
||||||
|
# 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}"
|
||||||
|
|
||||||
|
# Configure Grover options for PDF generation
|
||||||
|
pdf_options = {
|
||||||
|
format: 'A4',
|
||||||
|
margin: {
|
||||||
|
top: '0.5in',
|
||||||
|
bottom: '0.5in',
|
||||||
|
left: '0.5in',
|
||||||
|
right: '0.5in'
|
||||||
|
},
|
||||||
|
print_background: true,
|
||||||
|
display_header_footer: false,
|
||||||
|
prefer_css_page_size: true,
|
||||||
|
launch_args: ["--no-sandbox", "--disable-setuid-sandbox"] # For better compatibility
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate PDF
|
||||||
|
pdf = Grover.new(html, pdf_options).to_pdf
|
||||||
|
|
||||||
|
Rails.logger.info "PDF generation completed for ticket ID: #{@ticket.id}"
|
||||||
|
|
||||||
|
# Send PDF as download with SEO-friendly filename
|
||||||
|
send_data pdf,
|
||||||
|
filename: "billet-#{@ticket.event.slug}-#{@ticket.id}.pdf",
|
||||||
|
type: 'application/pdf',
|
||||||
|
disposition: 'attachment'
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "PDF generation failed for ticket ID: #{@ticket.id} - Error: #{e.message}"
|
||||||
|
Rails.logger.error e.backtrace.join("\n")
|
||||||
|
|
||||||
|
redirect_to view_ticket_path(event_slug: @ticket.event.slug, ticket_id: @ticket.id),
|
||||||
|
alert: "Erreur lors de la génération du PDF. Veuillez réessayer."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Redirect retry payment to order system
|
||||||
|
def retry_payment
|
||||||
|
# Look for draft order for this ticket's event
|
||||||
|
order = current_user.orders.find_by(event: @ticket.event, status: "draft")
|
||||||
|
|
||||||
|
if order&.can_retry_payment?
|
||||||
|
year = order.event.start_time.year
|
||||||
|
month = format("%02d", order.event.start_time.month)
|
||||||
|
redirect_to event_checkout_path(year: year, month: month, slug: order.event.slug)
|
||||||
|
else
|
||||||
|
redirect_to seo_event_path(@ticket.event),
|
||||||
|
alert: "Aucune commande disponible pour un nouveau paiement"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Legacy redirects for backward compatibility
|
||||||
|
def payment_success
|
||||||
|
redirect_to booking_payment_success_path(session_id: params[:session_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def payment_cancel
|
||||||
|
redirect_to booking_payment_cancelled_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_ticket_from_seo_params
|
||||||
|
# Parse event_slug and ticket_id from the SEO-friendly format: event-slug-123
|
||||||
|
slug_and_id = params[:event_slug_ticket_id] || "#{params[:event_slug]}-#{params[:ticket_id]}"
|
||||||
|
|
||||||
|
# Split by last dash to separate event slug from ticket ID
|
||||||
|
parts = slug_and_id.split('-')
|
||||||
|
ticket_id = parts.pop
|
||||||
|
event_slug = parts.join('-')
|
||||||
|
|
||||||
|
# Find ticket and ensure it belongs to current user
|
||||||
|
@ticket = Ticket.joins(order: :user)
|
||||||
|
.includes(:event, :ticket_type, order: :user)
|
||||||
|
.joins(:event)
|
||||||
|
.where(
|
||||||
|
tickets: { id: ticket_id },
|
||||||
|
orders: { user_id: current_user.id },
|
||||||
|
events: { slug: event_slug }
|
||||||
|
)
|
||||||
|
.first
|
||||||
|
|
||||||
|
unless @ticket
|
||||||
|
redirect_to dashboard_path, alert: "Billet non trouvé ou vous n'avez pas l'autorisation d'accéder à ce billet"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate SEO-friendly path for an event
|
||||||
|
def seo_event_path(event)
|
||||||
|
year = event.start_time.year
|
||||||
|
month = format("%02d", event.start_time.month)
|
||||||
|
event_path(year: year, month: month, slug: event.slug)
|
||||||
|
end
|
||||||
|
helper_method :seo_event_path
|
||||||
|
end
|
||||||
5
app/helpers/application_helper.rb
Normal file → Executable file
5
app/helpers/application_helper.rb
Normal file → Executable file
@@ -1,9 +1,12 @@
|
|||||||
module ApplicationHelper
|
module ApplicationHelper
|
||||||
# Convert prince from cents to float
|
# Convert price from cents to float
|
||||||
def format_price(cents)
|
def format_price(cents)
|
||||||
(cents.to_f / 100).round(2)
|
(cents.to_f / 100).round(2)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Include flash message helpers
|
# Include flash message helpers
|
||||||
include FlashMessagesHelper
|
include FlashMessagesHelper
|
||||||
|
|
||||||
|
# Include Stripe helper
|
||||||
|
include StripeHelper
|
||||||
end
|
end
|
||||||
|
|||||||
61
app/helpers/flash_messages_helper.rb
Normal file → Executable file
61
app/helpers/flash_messages_helper.rb
Normal file → Executable file
@@ -1,34 +1,51 @@
|
|||||||
|
# Flash messages helper for consistent styling across the application
|
||||||
|
#
|
||||||
|
# Provides standardized CSS classes and icons for different types of flash messages
|
||||||
|
# using Tailwind CSS classes and Lucide icons for consistent UI presentation
|
||||||
module FlashMessagesHelper
|
module FlashMessagesHelper
|
||||||
|
# Return appropriate Tailwind CSS classes for different flash message types
|
||||||
|
#
|
||||||
|
# @param type [String, Symbol] The flash message type (notice, error, warning, info)
|
||||||
|
# @return [String] Tailwind CSS classes for styling the flash message container
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# flash_class('success') # => "bg-green-50 text-green-800 border-green-200"
|
||||||
|
# flash_class('error') # => "bg-red-50 text-red-800 border-red-200"
|
||||||
def flash_class(type)
|
def flash_class(type)
|
||||||
case type.to_s
|
case type.to_s
|
||||||
when 'notice' then 'flash-message-success'
|
when "notice", "success"
|
||||||
when 'success' then 'flash-message-success'
|
"bg-green-50 text-green-800 border-green-200"
|
||||||
when 'error' then 'flash-message-error'
|
when "error", "alert"
|
||||||
when 'alert' then 'flash-message-error'
|
"bg-red-50 text-red-800 border-red-200"
|
||||||
when 'warning' then 'flash-message-warning'
|
when "warning"
|
||||||
when 'info' then 'flash-message-info'
|
"bg-yellow-50 text-yellow-800 border-yellow-200"
|
||||||
else "flash-message-#{type}"
|
when "info"
|
||||||
|
"bg-blue-50 text-blue-800 border-blue-200"
|
||||||
|
else
|
||||||
|
"bg-gray-50 text-gray-800 border-gray-200"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Return appropriate Lucide icon for different flash message types
|
||||||
|
#
|
||||||
|
# @param type [String, Symbol] The flash message type
|
||||||
|
# @return [String] HTML content tag with Lucide icon data attribute
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# flash_icon('success') # => <i data-lucide="check-circle" class="..."></i>
|
||||||
|
# flash_icon('error') # => <i data-lucide="x-circle" class="..."></i>
|
||||||
def flash_icon(type)
|
def flash_icon(type)
|
||||||
case type.to_s
|
case type.to_s
|
||||||
when 'notice', 'success'
|
when "notice", "success"
|
||||||
content_tag :svg, class: "h-5 w-5 text-green-400", fill: "currentColor", viewBox: "0 0 20 20" do
|
content_tag :i, "", "data-lucide": "check-circle", class: "w-5 h-5 flex-shrink-0"
|
||||||
content_tag :path, "", "fill-rule": "evenodd", "d": "M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z", "clip-rule": "evenodd"
|
when "error", "alert"
|
||||||
end
|
content_tag :i, "", "data-lucide": "x-circle", class: "w-5 h-5 flex-shrink-0"
|
||||||
when 'error', 'alert'
|
when "warning"
|
||||||
content_tag :svg, class: "h-5 w-5 text-red-400", fill: "currentColor", viewBox: "0 0 20 20" do
|
content_tag :i, "", "data-lucide": "alert-triangle", class: "w-5 h-5 flex-shrink-0"
|
||||||
content_tag :path, "", "fill-rule": "evenodd", "d": "M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z", "clip-rule": "evenodd"
|
when "info"
|
||||||
end
|
content_tag :i, "", "data-lucide": "info", class: "w-5 h-5 flex-shrink-0"
|
||||||
when 'warning'
|
|
||||||
content_tag :svg, class: "h-5 w-5 text-yellow-400", fill: "currentColor", viewBox: "0 0 20 20" do
|
|
||||||
content_tag :path, "", "fill-rule": "evenodd", "d": "M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z", "clip-rule": "evenodd"
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
content_tag :svg, class: "h-5 w-5 text-blue-400", fill: "currentColor", viewBox: "0 0 20 20" do
|
content_tag :i, "", "data-lucide": "bell", class: "w-5 h-5 flex-shrink-0"
|
||||||
content_tag :path, "", "fill-rule": "evenodd", "d": "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z", "clip-rule": "evenodd"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
82
app/helpers/lucide_helper.rb
Normal file
82
app/helpers/lucide_helper.rb
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
module LucideHelper
|
||||||
|
# Create a Lucide icon element
|
||||||
|
#
|
||||||
|
# @param name [String] The name of the Lucide icon
|
||||||
|
# @param options [Hash] Additional options
|
||||||
|
# @option options [String] :class Additional CSS classes
|
||||||
|
# @option options [String] :size Size class (e.g., 'w-4 h-4', 'w-6 h-6')
|
||||||
|
# @option options [Hash] :data Additional data attributes
|
||||||
|
#
|
||||||
|
# @return [String] HTML string for the icon
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# lucide_icon('user')
|
||||||
|
# lucide_icon('check-circle', class: 'text-green-500', size: 'w-5 h-5')
|
||||||
|
# lucide_icon('menu', data: { action: 'click->header#toggleMenu' })
|
||||||
|
def lucide_icon(name, options = {})
|
||||||
|
css_classes = [ "lucide-icon" ]
|
||||||
|
css_classes << options[:size] if options[:size]
|
||||||
|
css_classes << options[:class] if options[:class]
|
||||||
|
|
||||||
|
data_attributes = { lucide: name }
|
||||||
|
data_attributes.merge!(options[:data]) if options[:data]
|
||||||
|
|
||||||
|
content_tag :i, "",
|
||||||
|
class: css_classes.join(" "),
|
||||||
|
data: data_attributes,
|
||||||
|
**options.except(:class, :size, :data)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create a button with a Lucide icon
|
||||||
|
#
|
||||||
|
# @param name [String] The name of the Lucide icon
|
||||||
|
# @param options [Hash] Button options
|
||||||
|
# @option options [String] :text Button text (optional)
|
||||||
|
# @option options [String] :class Additional CSS classes for button
|
||||||
|
# @option options [String] :icon_class Additional CSS classes for icon
|
||||||
|
# @option options [String] :icon_size Size class for icon
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# lucide_button('plus', text: 'Add Item', class: 'btn btn-primary')
|
||||||
|
# lucide_button('trash-2', class: 'btn-danger', data: { confirm: 'Are you sure?' })
|
||||||
|
def lucide_button(name, options = {})
|
||||||
|
text = options.delete(:text)
|
||||||
|
icon_class = options.delete(:icon_class)
|
||||||
|
icon_size = options.delete(:icon_size) || "w-4 h-4"
|
||||||
|
|
||||||
|
icon = lucide_icon(name, class: icon_class, size: icon_size)
|
||||||
|
|
||||||
|
content = if text.present?
|
||||||
|
safe_join([ icon, " ", text ])
|
||||||
|
else
|
||||||
|
icon
|
||||||
|
end
|
||||||
|
|
||||||
|
content_tag :button, content, options
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create a link with a Lucide icon
|
||||||
|
#
|
||||||
|
# @param name [String] The name of the Lucide icon
|
||||||
|
# @param url [String] The URL for the link
|
||||||
|
# @param options [Hash] Link options
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# lucide_link('edit', edit_user_path(user), text: 'Edit')
|
||||||
|
# lucide_link('external-link', 'https://example.com', text: 'Visit', target: '_blank')
|
||||||
|
def lucide_link(name, url, options = {})
|
||||||
|
text = options.delete(:text)
|
||||||
|
icon_class = options.delete(:icon_class)
|
||||||
|
icon_size = options.delete(:icon_size) || "w-4 h-4"
|
||||||
|
|
||||||
|
icon = lucide_icon(name, class: icon_class, size: icon_size)
|
||||||
|
|
||||||
|
content = if text.present?
|
||||||
|
safe_join([ icon, " ", text ])
|
||||||
|
else
|
||||||
|
icon
|
||||||
|
end
|
||||||
|
|
||||||
|
link_to content, url, options
|
||||||
|
end
|
||||||
|
end
|
||||||
0
app/helpers/pages_helper.rb
Normal file → Executable file
0
app/helpers/pages_helper.rb
Normal file → Executable file
17
app/helpers/pdf_helper.rb
Normal file
17
app/helpers/pdf_helper.rb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
module PdfHelper
|
||||||
|
require "rqrcode"
|
||||||
|
|
||||||
|
# Generate SVG QR code for tickets
|
||||||
|
def qr_code_tag(data)
|
||||||
|
qrcode = RQRCode::QRCode.new(data)
|
||||||
|
|
||||||
|
# Render as SVG
|
||||||
|
raw qrcode.as_svg(
|
||||||
|
offset: 0,
|
||||||
|
color: "000",
|
||||||
|
shape_rendering: "crispEdges",
|
||||||
|
module_size: 4,
|
||||||
|
standalone: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
17
app/helpers/stripe_helper.rb
Normal file
17
app/helpers/stripe_helper.rb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
module StripeHelper
|
||||||
|
# Safely call Stripe methods with error handling
|
||||||
|
def safe_stripe_call(&block)
|
||||||
|
# Check if Stripe is properly configured
|
||||||
|
return nil unless Rails.application.config.stripe[:secret_key].present?
|
||||||
|
|
||||||
|
# Stripe is now initialized at application startup
|
||||||
|
Rails.logger.debug "Using globally initialized Stripe"
|
||||||
|
|
||||||
|
begin
|
||||||
|
yield if block_given?
|
||||||
|
rescue Stripe::StripeError => e
|
||||||
|
Rails.logger.error "Stripe Error: #{e.message}"
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
2
app/helpers/tickets_helper.rb
Normal file
2
app/helpers/tickets_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module TicketsHelper
|
||||||
|
end
|
||||||
20
app/javascript/application.js
Normal file → Executable file
20
app/javascript/application.js
Normal file → Executable file
@@ -1,3 +1,23 @@
|
|||||||
// Entry point for the build script in your package.json
|
// Entry point for the build script in your package.json
|
||||||
|
// This file initializes the Rails application with Turbo and Stimulus controllers
|
||||||
|
|
||||||
|
// Import Turbo Rails for SPA-like navigation
|
||||||
import "@hotwired/turbo-rails";
|
import "@hotwired/turbo-rails";
|
||||||
|
|
||||||
|
// Import all Stimulus controllers
|
||||||
import "./controllers";
|
import "./controllers";
|
||||||
|
|
||||||
|
// Import and initialize Lucide icons globally
|
||||||
|
import { createIcons, icons } from 'lucide';
|
||||||
|
|
||||||
|
// Initialize icons globally
|
||||||
|
function initializeLucideIcons() {
|
||||||
|
createIcons({ icons });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run on initial page load
|
||||||
|
document.addEventListener('DOMContentLoaded', initializeLucideIcons);
|
||||||
|
|
||||||
|
// Run on Turbo navigation (Rails 7+ SPA behavior)
|
||||||
|
document.addEventListener('turbo:render', initializeLucideIcons);
|
||||||
|
document.addEventListener('turbo:frame-render', initializeLucideIcons);
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { cva } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const buttonVariants = cva(
|
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default:
|
|
||||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
|
||||||
destructive:
|
|
||||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
||||||
outline:
|
|
||||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
||||||
secondary:
|
|
||||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
|
||||||
ghost:
|
|
||||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
||||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
||||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
||||||
icon: "size-9",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function Button({
|
|
||||||
className,
|
|
||||||
variant,
|
|
||||||
size,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="button"
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
{...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
|
||||||
14
app/javascript/controllers/application.js
Normal file → Executable file
14
app/javascript/controllers/application.js
Normal file → Executable file
@@ -1,14 +1,20 @@
|
|||||||
|
// Main Stimulus application controller
|
||||||
|
// Initializes the Stimulus framework and makes it available globally
|
||||||
import { Application } from "@hotwired/stimulus";
|
import { Application } from "@hotwired/stimulus";
|
||||||
import Alpine from "alpinejs";
|
|
||||||
|
|
||||||
|
// Create and start the Stimulus application
|
||||||
const application = Application.start();
|
const application = Application.start();
|
||||||
|
|
||||||
// Configure Stimulus development experience
|
// Configure Stimulus development experience
|
||||||
|
// Set to false in production to avoid unnecessary logging
|
||||||
application.debug = false;
|
application.debug = false;
|
||||||
|
|
||||||
|
// Make Stimulus globally available for debugging purposes
|
||||||
window.Stimulus = application;
|
window.Stimulus = application;
|
||||||
|
|
||||||
// Configure and load Alpine
|
// Configure Alpine js (commented out as it's not currently used)
|
||||||
window.Alpine = Alpine;
|
// import Alpine from "alpinejs";
|
||||||
Alpine.start();
|
// window.Alpine = Alpine;
|
||||||
|
// Alpine.start();
|
||||||
|
|
||||||
export { application };
|
export { application };
|
||||||
|
|||||||
58
app/javascript/controllers/counter_controller.js
Normal file → Executable file
58
app/javascript/controllers/counter_controller.js
Normal file → Executable file
@@ -1,61 +1,85 @@
|
|||||||
import { Controller } from "@hotwired/stimulus"
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
// Counter controller for animating number increments
|
||||||
|
// Used for statistics and numerical displays that animate when they come into view
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
|
// Define controller values with defaults
|
||||||
static values = {
|
static values = {
|
||||||
target: Number,
|
target: { type: Number, default: 0 }, // Target number to count to
|
||||||
decimal: Boolean,
|
decimal: { type: Boolean, default: false }, // Whether to display decimal values
|
||||||
duration: { type: Number, default: 2000 }
|
duration: { type: Number, default: 2000 } // Animation duration in milliseconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up the intersection observer when the controller connects
|
||||||
connect() {
|
connect() {
|
||||||
|
// Create an intersection observer to trigger animation when element is visible
|
||||||
this.observer = new IntersectionObserver((entries) => {
|
this.observer = new IntersectionObserver((entries) => {
|
||||||
entries.forEach(entry => {
|
entries.forEach(entry => {
|
||||||
|
// Start animation when element is 50% visible
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
this.animate()
|
this.animate()
|
||||||
|
// Stop observing after animation starts
|
||||||
this.observer.unobserve(this.element)
|
this.observer.unobserve(this.element)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, { threshold: 0.5 })
|
}, { threshold: 0.5 })
|
||||||
|
|
||||||
|
// Begin observing this element
|
||||||
this.observer.observe(this.element)
|
this.observer.observe(this.element)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up the observer when the controller disconnects
|
||||||
disconnect() {
|
disconnect() {
|
||||||
if (this.observer) {
|
if (this.observer) {
|
||||||
this.observer.disconnect()
|
this.observer.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Animate the counter from 0 to the target value
|
||||||
animate() {
|
animate() {
|
||||||
const startValue = 0
|
// Find the target element with data-target-value
|
||||||
const startTime = performance.now()
|
const targetElement = this.element.querySelector('.stat-number');
|
||||||
|
if (!targetElement) return;
|
||||||
|
|
||||||
|
// Get the target value
|
||||||
|
this.targetValue = parseInt(targetElement.getAttribute('data-target-value'), 10) || this.targetValue;
|
||||||
|
|
||||||
|
const startValue = 0;
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
// Update counter function using requestAnimationFrame for smooth animation
|
||||||
const updateCounter = (currentTime) => {
|
const updateCounter = (currentTime) => {
|
||||||
const elapsedTime = currentTime - startTime
|
const elapsedTime = currentTime - startTime;
|
||||||
const progress = Math.min(elapsedTime / this.durationValue, 1)
|
const progress = Math.min(elapsedTime / this.durationValue, 1);
|
||||||
|
|
||||||
// Easing function for smooth animation
|
// Easing function for smooth animation (ease-out quartic)
|
||||||
const easeOutQuart = 1 - Math.pow(1 - progress, 4)
|
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
|
||||||
|
|
||||||
let currentValue = startValue + (this.targetValue - startValue) * easeOutQuart
|
let currentValue = startValue + (this.targetValue - startValue) * easeOutQuart;
|
||||||
|
|
||||||
|
// Format value based on decimal setting
|
||||||
if (this.decimalValue && this.targetValue < 10) {
|
if (this.decimalValue && this.targetValue < 10) {
|
||||||
currentValue = currentValue.toFixed(1)
|
currentValue = currentValue.toFixed(1);
|
||||||
} else {
|
} else {
|
||||||
currentValue = Math.floor(currentValue)
|
currentValue = Math.floor(currentValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.element.textContent = currentValue
|
// Update only the text content of the target element
|
||||||
|
targetElement.textContent = currentValue;
|
||||||
|
|
||||||
|
// Continue animation until complete
|
||||||
if (progress < 1) {
|
if (progress < 1) {
|
||||||
requestAnimationFrame(updateCounter)
|
requestAnimationFrame(updateCounter);
|
||||||
} else {
|
} else {
|
||||||
this.element.textContent = this.decimalValue && this.targetValue < 10
|
// Ensure final value is exactly the target
|
||||||
|
const finalValue = this.decimalValue && this.targetValue < 10
|
||||||
? this.targetValue.toFixed(1)
|
? this.targetValue.toFixed(1)
|
||||||
: this.targetValue
|
: this.targetValue;
|
||||||
|
targetElement.textContent = finalValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(updateCounter)
|
// Start the animation
|
||||||
|
requestAnimationFrame(updateCounter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
app/javascript/controllers/flash_message_controller.js
Normal file → Executable file
41
app/javascript/controllers/flash_message_controller.js
Normal file → Executable file
@@ -1,27 +1,46 @@
|
|||||||
import { Controller } from "@hotwired/stimulus"
|
import { Controller } from "@hotwired/stimulus";
|
||||||
|
|
||||||
|
// Controller for handling flash messages
|
||||||
|
// Automatically dismisses messages after a timeout and handles manual closing
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = ["message"]
|
// Define targets for the controller
|
||||||
|
static targets = ["message"];
|
||||||
|
|
||||||
|
// Initialize the controller when it connects to the DOM
|
||||||
connect() {
|
connect() {
|
||||||
console.log("FlashMessageController mounted", this.element);
|
// console.log("FlashMessageController mounted", this.element);
|
||||||
|
console.log("FlashMessageController mounted");
|
||||||
|
|
||||||
// Auto-dismiss after 5 seconds
|
// Initialize Lucide icons for this element if available
|
||||||
this.timeout = setTimeout(() => {
|
if (typeof lucide !== "undefined") {
|
||||||
this.close()
|
lucide.createIcons({ within: this.element });
|
||||||
}, 5000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-dismiss after 2 seconds
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
this.close();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the timeout when the controller disconnects
|
||||||
disconnect() {
|
disconnect() {
|
||||||
if (this.timeout) {
|
if (this.timeout) {
|
||||||
clearTimeout(this.timeout)
|
clearTimeout(this.timeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close the flash message with a fade-out animation
|
||||||
close() {
|
close() {
|
||||||
this.element.classList.add('opacity-0', 'transition-opacity', 'duration-300')
|
// Add opacity transition classes
|
||||||
|
this.element.classList.add(
|
||||||
|
"opacity-0",
|
||||||
|
"transition-opacity",
|
||||||
|
"duration-300",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove element after transition completes
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.element.remove()
|
this.element.remove();
|
||||||
}, 300)
|
}, 300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
73
app/javascript/controllers/header_controller.js
Normal file
73
app/javascript/controllers/header_controller.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
// Controller for handling the header navigation
|
||||||
|
// Manages mobile menu toggle and user dropdown menu
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["mobileMenu", "mobileMenuButton", "userMenu", "userMenuButton"]
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
// Initialize menu states
|
||||||
|
this.mobileMenuOpen = false
|
||||||
|
this.userMenuOpen = false
|
||||||
|
|
||||||
|
// Add click outside listener for user menu
|
||||||
|
this.clickOutsideHandler = this.handleClickOutside.bind(this)
|
||||||
|
document.addEventListener("click", this.clickOutsideHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
// Clean up event listener
|
||||||
|
document.removeEventListener("click", this.clickOutsideHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle mobile menu visibility
|
||||||
|
toggleMobileMenu() {
|
||||||
|
this.mobileMenuOpen = !this.mobileMenuOpen
|
||||||
|
this.mobileMenuTarget.classList.toggle("hidden", !this.mobileMenuOpen)
|
||||||
|
|
||||||
|
// Update button icon based on state
|
||||||
|
const iconOpen = this.mobileMenuButtonTarget.querySelector('[data-menu-icon="open"]')
|
||||||
|
const iconClose = this.mobileMenuButtonTarget.querySelector('[data-menu-icon="close"]')
|
||||||
|
|
||||||
|
if (iconOpen && iconClose) {
|
||||||
|
iconOpen.classList.toggle("hidden", this.mobileMenuOpen)
|
||||||
|
iconClose.classList.toggle("hidden", !this.mobileMenuOpen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle user dropdown menu visibility
|
||||||
|
toggleUserMenu() {
|
||||||
|
this.userMenuOpen = !this.userMenuOpen
|
||||||
|
if (this.hasUserMenuTarget) {
|
||||||
|
this.userMenuTarget.classList.toggle("hidden", !this.userMenuOpen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close menus when clicking outside
|
||||||
|
handleClickOutside(event) {
|
||||||
|
// Close user menu if clicked outside
|
||||||
|
if (this.userMenuOpen && this.hasUserMenuTarget &&
|
||||||
|
!this.userMenuTarget.contains(event.target) &&
|
||||||
|
!this.userMenuButtonTarget.contains(event.target)) {
|
||||||
|
this.userMenuOpen = false
|
||||||
|
this.userMenuTarget.classList.add("hidden")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close mobile menu if clicked outside
|
||||||
|
if (this.mobileMenuOpen &&
|
||||||
|
!this.mobileMenuTarget.contains(event.target) &&
|
||||||
|
!this.mobileMenuButtonTarget.contains(event.target)) {
|
||||||
|
this.mobileMenuOpen = false
|
||||||
|
this.mobileMenuTarget.classList.add("hidden")
|
||||||
|
|
||||||
|
// Update button icon
|
||||||
|
const iconOpen = this.mobileMenuButtonTarget.querySelector('[data-menu-icon="open"]')
|
||||||
|
const iconClose = this.mobileMenuButtonTarget.querySelector('[data-menu-icon="close"]')
|
||||||
|
|
||||||
|
if (iconOpen && iconClose) {
|
||||||
|
iconOpen.classList.remove("hidden")
|
||||||
|
iconClose.classList.add("hidden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/javascript/controllers/index.js
Normal file → Executable file
22
app/javascript/controllers/index.js
Normal file → Executable file
@@ -2,15 +2,19 @@
|
|||||||
// Run that command whenever you add a new controller or create them with
|
// Run that command whenever you add a new controller or create them with
|
||||||
// ./bin/rails generate stimulus controllerName
|
// ./bin/rails generate stimulus controllerName
|
||||||
|
|
||||||
import { application } from "./application"
|
import { application } from "./application";
|
||||||
|
|
||||||
import LogoutController from "./logout_controller"
|
import LogoutController from "./logout_controller";
|
||||||
import FlashMessage from "./flash_message_controller"
|
application.register("logout", LogoutController);
|
||||||
import CounterController from "./counter_controller"
|
|
||||||
import ShadcnTestController from "./shadcn_test_controller"
|
|
||||||
|
|
||||||
application.register("logout", LogoutController) // Allow logout using js
|
import CounterController from "./counter_controller";
|
||||||
application.register("flash-message", FlashMessage) // Dismiss notification after 5 secondes
|
application.register("counter", CounterController);
|
||||||
application.register("counter", CounterController) // Simple counter for homepage
|
|
||||||
|
|
||||||
application.register("shadcn-test", ShadcnTestController) // Test controller for Shadcn
|
import FlashMessageController from "./flash_message_controller";
|
||||||
|
application.register("flash-message", FlashMessageController);
|
||||||
|
|
||||||
|
import TicketSelectionController from "./ticket_selection_controller";
|
||||||
|
application.register("ticket-selection", TicketSelectionController);
|
||||||
|
|
||||||
|
import HeaderController from "./header_controller";
|
||||||
|
application.register("header", HeaderController);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user