Merge branch 'feat/theme/header' into develop
This commit is contained in:
@@ -3,10 +3,50 @@
|
||||
/* Import Tailwind using PostCSS */
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Import flash message styles */
|
||||
@import "components/flash";
|
||||
/* Import our custom theme */
|
||||
@import "theme";
|
||||
|
||||
/** Default text color */
|
||||
/* Import components */
|
||||
@import "components/hero";
|
||||
@import "components/flash";
|
||||
@import "components/footer";
|
||||
@import "components/event-finder";
|
||||
|
||||
/* Import pages */
|
||||
@import "pages/home";
|
||||
|
||||
/* Base styles */
|
||||
body {
|
||||
color: #555555;
|
||||
font-family: var(--font-sans);
|
||||
line-height: 1.6;
|
||||
color: var(--color-neutral-900);
|
||||
background: var(--color-neutral-50);
|
||||
}
|
||||
|
||||
/* App wrapper */
|
||||
.app-wrapper {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 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%;
|
||||
}
|
||||
|
||||
210
app/assets/stylesheets/components/event-finder.css
Executable file
210
app/assets/stylesheets/components/event-finder.css
Executable file
@@ -0,0 +1,210 @@
|
||||
.event-finder {
|
||||
background: white;
|
||||
border-radius: var(--radius-2xl);
|
||||
box-shadow: var(--shadow-2xl);
|
||||
padding: var(--space-8);
|
||||
margin: var(--space-12) auto;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.finder-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--space-8);
|
||||
}
|
||||
|
||||
.finder-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 800;
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--color-neutral-900);
|
||||
}
|
||||
|
||||
.finder-subtitle {
|
||||
color: var(--color-neutral-600);
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.finder-form {
|
||||
display: grid;
|
||||
gap: var(--space-6);
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.finder-form {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.finder-form {
|
||||
grid-template-columns: repeat(4, 1fr) auto;
|
||||
align-items: end;
|
||||
}
|
||||
}
|
||||
|
||||
.finder-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.finder-label {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 700;
|
||||
color: var(--color-neutral-700);
|
||||
margin-bottom: var(--space-2);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.finder-input,
|
||||
.finder-select {
|
||||
background: var(--color-neutral-50);
|
||||
border: 2px solid var(--color-neutral-200);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
font-size: var(--text-base);
|
||||
font-weight: 500;
|
||||
color: var(--color-neutral-900);
|
||||
transition: all var(--duration-normal) var(--ease-out);
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.finder-input:focus,
|
||||
.finder-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary-500);
|
||||
background: white;
|
||||
box-shadow: 0 0 0 4px rgb(168 85 247 / 0.1);
|
||||
}
|
||||
|
||||
.finder-input::placeholder {
|
||||
color: var(--color-neutral-500);
|
||||
}
|
||||
|
||||
.finder-select {
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-position: right 12px center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 16px;
|
||||
padding-right: var(--space-12);
|
||||
}
|
||||
|
||||
.price-range {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.price-range-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 800;
|
||||
color: var(--color-primary-600);
|
||||
}
|
||||
|
||||
.price-slider {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-neutral-200);
|
||||
outline: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.price-slider::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--radius-full);
|
||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-accent-500) 100%);
|
||||
cursor: pointer;
|
||||
border: 3px solid white;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.price-slider::-moz-range-thumb {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--radius-full);
|
||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-accent-500) 100%);
|
||||
cursor: pointer;
|
||||
border: 3px solid white;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.finder-search-btn {
|
||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-accent-500) 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-5) var(--space-8);
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
transition: all var(--duration-normal) var(--ease-out);
|
||||
box-shadow: var(--shadow-purple-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.finder-search-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-purple-lg);
|
||||
}
|
||||
|
||||
.finder-search-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.event-finder {
|
||||
margin: var(--space-8) auto;
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.finder-form {
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.finder-search-btn {
|
||||
justify-content: center;
|
||||
padding: var(--space-4) var(--space-6);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced animations */
|
||||
.animate-fadeInUp {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.6s var(--ease-out);
|
||||
}
|
||||
|
||||
.animate-fadeInUp.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Loading spinner animation */
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
82
app/assets/stylesheets/components/footer.css
Executable file
82
app/assets/stylesheets/components/footer.css
Executable file
@@ -0,0 +1,82 @@
|
||||
|
||||
.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
238
app/assets/stylesheets/components/hero.css
Executable file
238
app/assets/stylesheets/components/hero.css
Executable file
@@ -0,0 +1,238 @@
|
||||
.hero {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(168, 85, 247, 0.08) 0%,
|
||||
rgba(236, 72, 153, 0.08) 50%,
|
||||
rgba(168, 85, 247, 0.08) 100%);
|
||||
padding: var(--space-12) 0 var(--space-8);
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(168, 85, 247, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(236, 72, 153, 0.1) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(2rem, 5vw, 3.5rem);
|
||||
font-weight: 900;
|
||||
margin-bottom: var(--space-4);
|
||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-accent-500) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.hero .subtitle {
|
||||
font-size: var(--text-lg);
|
||||
color: var(--color-neutral-600);
|
||||
margin-bottom: var(--space-6);
|
||||
max-width: 700px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cta-group {
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--space-8);
|
||||
}
|
||||
|
||||
.hero-stats {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--space-6);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hero-stat {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-stat-number {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 800;
|
||||
color: var(--color-primary-600);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hero-stat-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-neutral-500);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: var(--space-12) 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--space-8);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.5rem, 3vw, 2.5rem);
|
||||
font-weight: 800;
|
||||
margin-bottom: var(--space-3);
|
||||
color: var(--color-neutral-900);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
font-size: var(--text-lg);
|
||||
color: var(--color-neutral-600);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.features-section {
|
||||
background: linear-gradient(135deg, var(--color-primary-50) 0%, var(--color-accent-50) 100%);
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
gap: var(--space-6);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: white;
|
||||
padding: var(--space-6);
|
||||
border-radius: var(--radius-xl);
|
||||
text-align: center;
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
transition: all var(--duration-normal) var(--ease-out);
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: linear-gradient(135deg, var(--color-primary-500) 0%, var(--color-accent-400) 100%);
|
||||
color: white;
|
||||
border-radius: var(--radius-xl);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto var(--space-3);
|
||||
box-shadow: var(--shadow-purple-sm);
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 700;
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--color-neutral-900);
|
||||
}
|
||||
|
||||
.feature-description {
|
||||
color: var(--color-neutral-600);
|
||||
line-height: 1.6;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
background: var(--color-neutral-900);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
gap: var(--space-6);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.5rem, 4vw, 2.5rem);
|
||||
font-weight: 900;
|
||||
color: var(--color-accent-400);
|
||||
display: block;
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--color-neutral-300);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.cta-section {
|
||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-accent-500) 100%);
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: var(--space-12) 0;
|
||||
}
|
||||
|
||||
.cta-content h2 {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.5rem, 3vw, 2rem);
|
||||
font-weight: 800;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.cta-content p {
|
||||
font-size: var(--text-lg);
|
||||
margin-bottom: var(--space-6);
|
||||
opacity: 0.9;
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -1,161 +1,790 @@
|
||||
/* Theme Rules from docs/theme-rules.md - Light Theme Only */
|
||||
|
||||
/* Custom properties for the design system */
|
||||
:root {
|
||||
/* Primary - Purple gradient system */
|
||||
--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;
|
||||
/* Colors */
|
||||
--color-primary-50: #f5f3ff;
|
||||
--color-primary-100: #ede9fe;
|
||||
--color-primary-200: #ddd6fe;
|
||||
--color-primary-300: #c4b5fd;
|
||||
--color-primary-400: #a78bfa;
|
||||
--color-primary-500: #8b5cf6;
|
||||
--color-primary-600: #7c3aed;
|
||||
--color-primary-700: #6d28d9;
|
||||
--color-primary-800: #5b21b6;
|
||||
--color-primary-900: #4c1d95;
|
||||
|
||||
/* Accent - Pink gradient */
|
||||
--color-accent-400: #f472b6;
|
||||
--color-accent-500: #ec4899;
|
||||
--color-accent-600: #db2777;
|
||||
--color-accent-50: #fdf2f8;
|
||||
--color-accent-100: #fce7f3;
|
||||
--color-accent-200: #fbcfe8;
|
||||
--color-accent-300: #f9a8d4;
|
||||
--color-accent-400: #f472b6;
|
||||
--color-accent-500: #ec4899;
|
||||
--color-accent-600: #db2777;
|
||||
--color-accent-700: #be185d;
|
||||
--color-accent-800: #9d174d;
|
||||
--color-accent-900: #831843;
|
||||
|
||||
/* Neutral - Slate system */
|
||||
--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-50: #fafafa;
|
||||
--color-neutral-100: #f5f5f5;
|
||||
--color-neutral-200: #e5e5e5;
|
||||
--color-neutral-300: #d4d4d4;
|
||||
--color-neutral-400: #a3a3a3;
|
||||
--color-neutral-500: #737373;
|
||||
--color-neutral-600: #525252;
|
||||
--color-neutral-700: #404040;
|
||||
--color-neutral-800: #262626;
|
||||
--color-neutral-900: #171717;
|
||||
|
||||
/* Font families */
|
||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--color-success: #10b981;
|
||||
--color-success-light: #d1fae5;
|
||||
--color-success-dark: #047857;
|
||||
|
||||
/* Spacing scale */
|
||||
--space-1: 0.25rem; /* 4px */
|
||||
--space-2: 0.5rem; /* 8px */
|
||||
--space-3: 0.75rem; /* 12px */
|
||||
--space-4: 1rem; /* 16px */
|
||||
--space-5: 1.25rem; /* 20px */
|
||||
--space-6: 1.5rem; /* 24px */
|
||||
--space-8: 2rem; /* 32px */
|
||||
--space-10: 2.5rem; /* 40px */
|
||||
--space-12: 3rem; /* 48px */
|
||||
--color-warning: #f59e0b;
|
||||
--color-warning-light: #fef3c7;
|
||||
--color-warning-dark: #b45309;
|
||||
|
||||
--color-danger: #ef4444;
|
||||
--color-danger-light: #fee2e2;
|
||||
--color-danger-dark: #b91c1c;
|
||||
|
||||
/* Typography */
|
||||
--font-sans: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
--font-display: 'Outfit', var(--font-sans);
|
||||
|
||||
--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 */
|
||||
|
||||
/* Spacing */
|
||||
--space-1: 0.25rem; /* 4px */
|
||||
--space-2: 0.5rem; /* 8px */
|
||||
--space-3: 0.75rem; /* 12px */
|
||||
--space-4: 1rem; /* 16px */
|
||||
--space-5: 1.25rem; /* 20px */
|
||||
--space-6: 1.5rem; /* 24px */
|
||||
--space-8: 2rem; /* 32px */
|
||||
--space-10: 2.5rem; /* 40px */
|
||||
--space-12: 3rem; /* 48px */
|
||||
--space-16: 4rem; /* 64px */
|
||||
--space-20: 5rem; /* 80px */
|
||||
|
||||
/* Radius */
|
||||
--radius: 0.25rem; /* 4px */
|
||||
--radius-md: 0.5rem; /* 8px */
|
||||
--radius-lg: 0.75rem; /* 12px */
|
||||
--radius-xl: 1rem; /* 16px */
|
||||
--radius-2xl: 1.5rem; /* 24px */
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
|
||||
--shadow-purple-sm: 0 1px 3px 0 rgba(168, 85, 247, 0.1), 0 1px 2px 0 rgba(168, 85, 247, 0.06);
|
||||
--shadow-purple-md: 0 4px 6px -1px rgba(168, 85, 247, 0.1), 0 2px 4px -1px rgba(168, 85, 247, 0.06);
|
||||
--shadow-purple-lg: 0 10px 15px -3px rgba(168, 85, 247, 0.1), 0 4px 6px -2px rgba(168, 85, 247, 0.05);
|
||||
|
||||
/* Transitions */
|
||||
--duration-fast: 150ms;
|
||||
--duration-normal: 300ms;
|
||||
--duration-slow: 500ms;
|
||||
|
||||
--ease-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
line-height: 1.6;
|
||||
color: var(--color-neutral-900);
|
||||
background: var(--color-neutral-50);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--space-4);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--text-4xl);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-lg);
|
||||
transition: all var(--duration-normal) var(--ease-out);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.btn-md {
|
||||
padding: var(--space-3) var(--space-6);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: var(--space-4) var(--space-8);
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
/* Button components */
|
||||
.btn-primary {
|
||||
@apply bg-gradient-to-r from-purple-600 to-pink-600 text-white font-medium py-2 px-4 rounded-lg shadow-sm hover:shadow-md transition-all duration-200;
|
||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-accent-500) 100%);
|
||||
color: white;
|
||||
box-shadow: var(--shadow-purple-md);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-purple-lg);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply bg-white text-purple-600 border border-purple-200 font-medium py-2 px-4 rounded-lg hover:bg-purple-50 transition-colors duration-200;
|
||||
background: var(--color-neutral-800);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-destructive {
|
||||
@apply bg-red-600 text-white font-medium py-2 px-4 rounded-lg shadow-sm hover:bg-red-700 transition-colors duration-200;
|
||||
.btn-secondary:hover {
|
||||
background: var(--color-neutral-900);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Card components */
|
||||
.card {
|
||||
@apply bg-white rounded-lg shadow-sm border border-neutral-200 p-6 hover:shadow-md transition-shadow duration-200;
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 2px solid var(--color-primary-600);
|
||||
color: var(--color-primary-600);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
@apply pb-4 border-b border-neutral-200 mb-4;
|
||||
.btn-outline:hover {
|
||||
background: var(--color-primary-50);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
@apply space-y-4;
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--color-neutral-700);
|
||||
}
|
||||
|
||||
/* Form components */
|
||||
.form-input {
|
||||
@apply block w-full rounded-md border-neutral-300 shadow-sm focus:border-purple-500 focus:ring-purple-500 sm:text-sm;
|
||||
.btn-ghost:hover {
|
||||
background: var(--color-neutral-100);
|
||||
}
|
||||
|
||||
/* Form elements */
|
||||
.form-group {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@apply block text-sm font-medium text-neutral-700 mb-1;
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 700;
|
||||
color: var(--color-neutral-700);
|
||||
margin-bottom: var(--space-2);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
background: var(--color-neutral-50);
|
||||
border: 2px solid var(--color-neutral-200);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
font-size: var(--text-base);
|
||||
font-weight: 500;
|
||||
color: var(--color-neutral-900);
|
||||
transition: all var(--duration-normal) var(--ease-out);
|
||||
font-family: var(--font-sans);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-select:focus,
|
||||
.form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary-500);
|
||||
background: white;
|
||||
box-shadow: 0 0 0 4px rgba(168, 85, 247, 0.1);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--color-neutral-500);
|
||||
}
|
||||
|
||||
.form-select {
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-position: right 12px center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 16px;
|
||||
padding-right: var(--space-12);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
@apply text-sm text-red-600 mt-1;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.nav-link {
|
||||
@apply text-neutral-600 hover:text-purple-600 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200;
|
||||
}
|
||||
|
||||
.nav-link-active {
|
||||
@apply text-purple-600 bg-purple-50;
|
||||
}
|
||||
|
||||
/* Layout utilities */
|
||||
.container {
|
||||
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
|
||||
}
|
||||
|
||||
.grid-responsive {
|
||||
@apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6;
|
||||
}
|
||||
|
||||
.grid-cards {
|
||||
@apply grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6;
|
||||
}
|
||||
|
||||
/* Animation utilities */
|
||||
.hover-lift {
|
||||
@apply transition-transform duration-200 hover:-translate-y-1;
|
||||
}
|
||||
|
||||
.hover-glow {
|
||||
@apply transition-all duration-200 hover:shadow-lg hover:shadow-purple-500/25;
|
||||
color: var(--color-danger);
|
||||
font-size: var(--text-sm);
|
||||
margin-top: var(--space-1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.focus-ring {
|
||||
@apply focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2;
|
||||
transition: all var(--duration-normal) var(--ease-out);
|
||||
}
|
||||
|
||||
.transition-fast {
|
||||
@apply transition-all duration-150 ease-in-out;
|
||||
.focus-ring:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary-500);
|
||||
background: white;
|
||||
box-shadow: 0 0 0 4px rgba(168, 85, 247, 0.1);
|
||||
}
|
||||
|
||||
.transition-normal {
|
||||
@apply transition-all duration-200 ease-in-out;
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: var(--space-1) var(--space-3);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.transition-slow {
|
||||
@apply transition-all duration-300 ease-in-out;
|
||||
.badge-available {
|
||||
background: var(--color-success-light);
|
||||
color: var(--color-success-dark);
|
||||
border: 1px solid var(--color-success);
|
||||
}
|
||||
|
||||
/* State utilities */
|
||||
.disabled {
|
||||
@apply opacity-50 cursor-not-allowed;
|
||||
.badge-limited {
|
||||
background: var(--color-warning-light);
|
||||
color: var(--color-warning-dark);
|
||||
border: 1px solid var(--color-warning);
|
||||
}
|
||||
|
||||
.animate-pulse-subtle {
|
||||
@apply animate-pulse;
|
||||
animation-duration: 3s;
|
||||
.badge-sold-out {
|
||||
background: var(--color-danger-light);
|
||||
color: var(--color-danger-dark);
|
||||
border: 1px solid var(--color-danger);
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
@apply animate-in fade-in-0 duration-500;
|
||||
.badge-featured {
|
||||
background: var(--color-accent-100);
|
||||
color: var(--color-accent-700);
|
||||
border: 1px solid var(--color-accent-300);
|
||||
}
|
||||
|
||||
/* Accessibility utilities */
|
||||
.focus-visible {
|
||||
@apply focus:outline-none focus-visible:ring-2 focus-visible:ring-purple-500 focus-visible:ring-offset-2;
|
||||
.badge-vip {
|
||||
background: var(--color-primary-100);
|
||||
color: var(--color-primary-800);
|
||||
border: 1px solid var(--color-primary-300);
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
@apply text-neutral-900;
|
||||
/* Cards */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-6);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all var(--duration-slow) var(--ease-out);
|
||||
}
|
||||
|
||||
.card.hover-lift:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: var(--shadow-2xl);
|
||||
border-color: var(--color-primary-200);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.event-card.hover-glow:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: var(--shadow-2xl);
|
||||
border-color: var(--color-primary-200);
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.nav {
|
||||
background: white;
|
||||
box-shadow: var(--shadow-sm);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: var(--color-neutral-700);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
padding: var(--space-3) var(--space-2);
|
||||
border-radius: var(--radius);
|
||||
transition: all var(--duration-normal);
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--color-primary-600);
|
||||
background: var(--color-primary-50);
|
||||
}
|
||||
|
||||
/* Search box */
|
||||
.search-box {
|
||||
position: relative;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding-left: var(--space-12);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: var(--space-4);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-neutral-400);
|
||||
}
|
||||
|
||||
/* Price tags */
|
||||
.price-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
font-family: var(--font-display);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.price-tag-sm {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.price-tag-md {
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.price-tag-lg {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
|
||||
/* Avatars */
|
||||
.avatar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-primary-100);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary-600);
|
||||
border: 2px solid white;
|
||||
box-shadow: var(--shadow-sm);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.avatar-lg {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.avatar-sm {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
.avatar-status {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: var(--radius-full);
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.status-online { background: var(--color-success); }
|
||||
.status-offline { background: var(--color-neutral-400); }
|
||||
.status-busy { background: var(--color-danger); }
|
||||
|
||||
/* Progress bars */
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: var(--color-neutral-200);
|
||||
border-radius: var(--radius-full);
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, var(--color-primary-500) 0%, var(--color-accent-400) 100%);
|
||||
border-radius: var(--radius-full);
|
||||
transition: width var(--duration-slow) var(--ease-out);
|
||||
}
|
||||
|
||||
/* Rating stars */
|
||||
.rating {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rating-star {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.rating-star.filled {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.rating-star.empty {
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
/* Notifications */
|
||||
.notification {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-lg);
|
||||
margin-bottom: var(--space-3);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.notification-success {
|
||||
background: var(--color-success-light);
|
||||
color: var(--color-success-dark);
|
||||
border: 1px solid var(--color-success);
|
||||
}
|
||||
|
||||
.notification-warning {
|
||||
background: var(--color-warning-light);
|
||||
color: var(--color-warning-dark);
|
||||
border: 1px solid var(--color-warning);
|
||||
}
|
||||
|
||||
.notification-error {
|
||||
background: var(--color-danger-light);
|
||||
color: var(--color-danger-dark);
|
||||
border: 1px solid var(--color-danger);
|
||||
}
|
||||
|
||||
.notification-info {
|
||||
background: var(--color-primary-50);
|
||||
color: var(--color-primary-800);
|
||||
border: 1px solid var(--color-primary-200);
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tabs {
|
||||
border-bottom: 1px solid var(--color-neutral-200);
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.tab-list {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
font-weight: 500;
|
||||
color: var(--color-neutral-600);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all var(--duration-normal);
|
||||
font-family: var(--font-display);
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
color: var(--color-primary-600);
|
||||
background: var(--color-primary-50);
|
||||
border-radius: var(--radius-md) var(--radius-md) 0 0;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
color: var(--color-primary-600);
|
||||
border-bottom-color: var(--color-primary-600);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
padding: var(--space-4) 0;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Tooltips */
|
||||
.tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
position: absolute;
|
||||
bottom: 125%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--color-neutral-900);
|
||||
color: white;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--text-sm);
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all var(--duration-normal);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tooltip-content::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 5px solid transparent;
|
||||
border-top-color: var(--color-neutral-900);
|
||||
}
|
||||
|
||||
.tooltip:hover .tooltip-content {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Loading states */
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid var(--color-neutral-200);
|
||||
border-top-color: var(--color-primary-600);
|
||||
border-radius: var(--radius-full);
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.loading-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--color-primary-600);
|
||||
border-radius: var(--radius-full);
|
||||
animation: bounce 1.4s infinite both;
|
||||
}
|
||||
|
||||
.loading-dot:nth-child(2) { animation-delay: 0.2s; }
|
||||
.loading-dot:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 80%, 100% { transform: scale(0); }
|
||||
40% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Breadcrumbs */
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
color: var(--color-neutral-600);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb-item:hover {
|
||||
color: var(--color-primary-600);
|
||||
}
|
||||
|
||||
.breadcrumb-item.current {
|
||||
color: var(--color-neutral-900);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
color: var(--color-neutral-400);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 var(--space-3);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--text-3xl);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: var(--space-3) var(--space-6);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
.btn-md {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
padding: var(--space-3);
|
||||
}
|
||||
}
|
||||
|
||||
/* Additional styles for enhanced Aperonight design */
|
||||
.event-card.hover-glow:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: var(--shadow-2xl);
|
||||
border-color: var(--color-primary-200);
|
||||
}
|
||||
|
||||
.price-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
font-family: var(--font-display);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.price-tag-sm {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.price-tag-md {
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.price-tag-lg {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding-left: var(--space-12);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: var(--space-4);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-neutral-400);
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
@apply text-neutral-600;
|
||||
}
|
||||
83
app/controllers/api/v1/events_controller.rb
Executable file
83
app/controllers/api/v1/events_controller.rb
Executable file
@@ -0,0 +1,83 @@
|
||||
# Contrôleur API pour la gestion des ressources d'événements
|
||||
# Fournit des points de terminaison RESTful pour les opérations CRUD sur le modèle Event
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class EventsController < ApiController
|
||||
# Charge l'évén avant certaines actions pour réduire les duplications
|
||||
before_action :set_event, only: [ :show, :update, :destroy ]
|
||||
|
||||
# GET /api/v1/events
|
||||
# Récupère tous les événements triés par date de création (du plus récent au plus ancien)
|
||||
def index
|
||||
@events = Event.all.order(created_at: :desc)
|
||||
render json: @events, status: :ok
|
||||
end
|
||||
|
||||
# GET /api/v1/events/:id
|
||||
# Récupère un seul événement par son ID
|
||||
# Retourne 404 si l'événement n'est pas trouvé
|
||||
def show
|
||||
render json: @event, status: :ok
|
||||
end
|
||||
|
||||
# POST /api/v1/events
|
||||
# Crée un nouvel événement avec les attributs fournis
|
||||
# Retourne 201 Created en cas de succès avec les données de l'événement
|
||||
# Retourne 422 Unprocessable Entity avec les messages d'erreur en cas d'échec
|
||||
def create
|
||||
@event = Event.new(event_params)
|
||||
if @event.save
|
||||
render json: @event, status: :created
|
||||
else
|
||||
render json: { errors: @event.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# PATCH/PUT /api/v1/events/:id
|
||||
# Met à jour un événement existant avec les attributs fournis
|
||||
# Retourne 200 OK avec les données mises à jour en cas de succès
|
||||
# Retourne 422 Unprocessable Entity avec les messages d'erreur en cas d'échec
|
||||
def update
|
||||
if @event.update(event_params)
|
||||
render json: @event, status: :ok
|
||||
else
|
||||
render json: { errors: @event.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /api/v1/events/:id
|
||||
# Supprime définitivement un événement
|
||||
# Retourne 204 No Content en cas de succès
|
||||
def destroy
|
||||
@event.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Trouve un événement par son ID ou retourne 404 Introuvable
|
||||
# Utilisé comme before_action pour les actions show, update et destroy
|
||||
def set_event
|
||||
@event = Event.find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render json: { error: "Événement non trouvé" }, status: :not_found
|
||||
end
|
||||
|
||||
# Paramètres forts pour la création et la mise à jour des événements
|
||||
# Liste blanche des attributs autorisés pour éviter les vulnérabilités de mass assignment
|
||||
def event_params
|
||||
params.require(:event).permit(
|
||||
:name,
|
||||
:description,
|
||||
:state,
|
||||
:venue_name,
|
||||
:venue_address,
|
||||
:latitude,
|
||||
:longitude,
|
||||
:featured
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,82 +0,0 @@
|
||||
# API controller for managing party resources
|
||||
# Provides RESTful endpoints for CRUD operations on Party model
|
||||
module Api
|
||||
module V1
|
||||
class PartiesController < ApiController
|
||||
# Load party before specific actions to reduce duplication
|
||||
before_action :set_party, only: [ :show, :update, :destroy ]
|
||||
|
||||
# GET /api/v1/parties
|
||||
# Returns all parties sorted by creation date (newest first)
|
||||
def index
|
||||
@parties = Party.all.order(created_at: :desc)
|
||||
render json: @parties, status: :ok
|
||||
end
|
||||
|
||||
# GET /api/v1/parties/:id
|
||||
# Returns a single party by ID
|
||||
# Returns 404 if party is not found
|
||||
def show
|
||||
render json: @party, status: :ok
|
||||
end
|
||||
|
||||
# POST /api/v1/parties
|
||||
# Creates a new party with provided attributes
|
||||
# Returns 201 Created on success with party data
|
||||
# Returns 422 Unprocessable Entity with validation errors on failure
|
||||
def create
|
||||
@party = Party.new(party_params)
|
||||
if @party.save
|
||||
render json: @party, status: :created
|
||||
else
|
||||
render json: { errors: @party.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# PATCH/PUT /api/v1/parties/:id
|
||||
# Updates an existing party with provided attributes
|
||||
# Returns 200 OK with updated party data on success
|
||||
# Returns 422 Unprocessable Entity with validation errors on failure
|
||||
def update
|
||||
if @party.update(party_params)
|
||||
render json: @party, status: :ok
|
||||
else
|
||||
render json: { errors: @party.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /api/v1/parties/:id
|
||||
# Permanently deletes a party
|
||||
# Returns 204 No Content on success
|
||||
def destroy
|
||||
@party.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Finds a party by ID or returns 404 Not Found
|
||||
# Used as before_action for show, update, and destroy actions
|
||||
def set_party
|
||||
@party = Party.find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render json: { error: "Party not found" }, status: :not_found
|
||||
end
|
||||
|
||||
# Strong parameters for party creation and updates
|
||||
# Whitelists permitted attributes to prevent mass assignment vulnerabilities
|
||||
def party_params
|
||||
params.require(:party).permit(
|
||||
:name,
|
||||
:description,
|
||||
:state,
|
||||
:venue_name,
|
||||
:venue_address,
|
||||
:latitude,
|
||||
:longitude,
|
||||
:featured
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,22 +1,22 @@
|
||||
class PartiesController < ApplicationController
|
||||
class EventsController < ApplicationController
|
||||
# Display all events
|
||||
def index
|
||||
@parties = Party.includes(:user).upcoming.page(params[:page]).per(1)
|
||||
# @parties = Party.page(params[:page]).per(12)
|
||||
@events = Event.includes(:user).upcoming.page(params[:page]).per(1)
|
||||
# @events = Event.page(params[:page]).per(12)
|
||||
end
|
||||
|
||||
# Display desired event
|
||||
def show
|
||||
@party = Party.find(params[:id])
|
||||
@event = Event.find(params[:id])
|
||||
end
|
||||
|
||||
# Handle checkout process
|
||||
def checkout
|
||||
@party = Party.find(params[:id])
|
||||
@event = Event.find(params[:id])
|
||||
cart_data = JSON.parse(params[:cart] || "{}")
|
||||
|
||||
if cart_data.empty?
|
||||
redirect_to party_path(@party), alert: "Please select at least one ticket"
|
||||
redirect_to event_path(@event), alert: "Please select at least one ticket"
|
||||
return
|
||||
end
|
||||
|
||||
@@ -25,7 +25,7 @@ class PartiesController < ApplicationController
|
||||
total_amount = 0
|
||||
|
||||
cart_data.each do |ticket_type_id, item|
|
||||
ticket_type = @party.ticket_types.find_by(id: ticket_type_id)
|
||||
ticket_type = @event.ticket_types.find_by(id: ticket_type_id)
|
||||
next unless ticket_type
|
||||
|
||||
quantity = item["quantity"].to_i
|
||||
@@ -34,7 +34,7 @@ class PartiesController < ApplicationController
|
||||
# Check availability
|
||||
available = ticket_type.quantity - ticket_type.tickets.count
|
||||
if quantity > available
|
||||
redirect_to party_path(@party), alert: "Not enough tickets available for #{ticket_type.name}"
|
||||
redirect_to event_path(@event), alert: "Not enough tickets available for #{ticket_type.name}"
|
||||
return
|
||||
end
|
||||
|
||||
@@ -48,7 +48,7 @@ class PartiesController < ApplicationController
|
||||
end
|
||||
|
||||
if order_items.empty?
|
||||
redirect_to party_path(@party), alert: "Invalid order"
|
||||
redirect_to event_path(@event), alert: "Invalid order"
|
||||
return
|
||||
end
|
||||
|
||||
@@ -59,6 +59,6 @@ class PartiesController < ApplicationController
|
||||
|
||||
# For now, we'll just redirect with a success message
|
||||
# In a real app, you'd redirect to a payment page
|
||||
redirect_to party_path(@party), notice: "Order created successfully! Proceeding to payment..."
|
||||
redirect_to event_path(@event), notice: "Order created successfully! Proceeding to payment..."
|
||||
end
|
||||
end
|
||||
@@ -5,10 +5,10 @@ class PagesController < ApplicationController
|
||||
# skip_before_action :authenticate_user!, only: [ :home ]
|
||||
before_action :authenticate_user!, only: [ :dashboard ]
|
||||
|
||||
# Homepage showing featured parties
|
||||
# Homepage showing featured events
|
||||
def home
|
||||
# @parties = Party.published.featured.limit(3)
|
||||
@parties = Party.where(state: :published).order(created_at: :desc)
|
||||
# @events = Event.published.featured.limit(3)
|
||||
# @events = Event.where(state: :published).order(created_at: :desc)
|
||||
|
||||
if user_signed_in?
|
||||
return redirect_to(dashboard_path)
|
||||
@@ -18,15 +18,15 @@ class PagesController < ApplicationController
|
||||
# User dashboard showing personalized content
|
||||
# Accessible only to authenticated users
|
||||
def dashboard
|
||||
@available_parties = Party.published.count
|
||||
@events_this_week = Party.published.where("start_time BETWEEN ? AND ?", Date.current.beginning_of_week, Date.current.end_of_week).count
|
||||
@today_parties = Party.published.where("DATE(start_time) = ?", Date.current).order(start_time: :asc)
|
||||
@tomorrow_parties = Party.published.where("DATE(start_time) = ?", Date.current + 1).order(start_time: :asc)
|
||||
@other_parties = Party.published.upcoming.where.not("DATE(start_time) IN (?)", [Date.current, Date.current + 1]).order(start_time: :asc).page(params[:page])
|
||||
@available_events = Event.published.count
|
||||
@events_this_week = Event.published.where("start_time BETWEEN ? AND ?", Date.current.beginning_of_week, Date.current.end_of_week).count
|
||||
@today_events = Event.published.where("DATE(start_time) = ?", Date.current).order(start_time: :asc)
|
||||
@tomorrow_events = Event.published.where("DATE(start_time) = ?", Date.current + 1).order(start_time: :asc)
|
||||
@other_events = Event.published.upcoming.where.not("DATE(start_time) IN (?)", [Date.current, Date.current + 1]).order(start_time: :asc).page(params[:page])
|
||||
end
|
||||
|
||||
# Events page showing all published parties with pagination
|
||||
# Events page showing all published events with pagination
|
||||
def events
|
||||
@parties = Party.published.order(created_at: :desc).page(params[:page])
|
||||
@events = Event.published.order(created_at: :desc).page(params[:page])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
target: Number,
|
||||
decimal: Boolean,
|
||||
static values = {
|
||||
target: { type: Number, default: 0 },
|
||||
decimal: { type: Boolean, default: false },
|
||||
duration: { type: Number, default: 2000 }
|
||||
}
|
||||
|
||||
@@ -27,35 +27,44 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
animate() {
|
||||
const startValue = 0
|
||||
const startTime = performance.now()
|
||||
// Find the target element with data-target-value
|
||||
const targetElement = this.element.querySelector('.stat-number');
|
||||
if (!targetElement) return;
|
||||
|
||||
// Get the target value
|
||||
this.targetValue = parseInt(targetElement.getAttribute('data-target-value'), 10) || this.targetValue;
|
||||
|
||||
const startValue = 0;
|
||||
const startTime = performance.now();
|
||||
|
||||
const updateCounter = (currentTime) => {
|
||||
const elapsedTime = currentTime - startTime
|
||||
const progress = Math.min(elapsedTime / this.durationValue, 1)
|
||||
|
||||
const elapsedTime = currentTime - startTime;
|
||||
const progress = Math.min(elapsedTime / this.durationValue, 1);
|
||||
|
||||
// Easing function for smooth animation
|
||||
const easeOutQuart = 1 - Math.pow(1 - progress, 4)
|
||||
|
||||
let currentValue = startValue + (this.targetValue - startValue) * easeOutQuart
|
||||
|
||||
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
|
||||
|
||||
let currentValue = startValue + (this.targetValue - startValue) * easeOutQuart;
|
||||
|
||||
if (this.decimalValue && this.targetValue < 10) {
|
||||
currentValue = currentValue.toFixed(1)
|
||||
currentValue = currentValue.toFixed(1);
|
||||
} else {
|
||||
currentValue = Math.floor(currentValue)
|
||||
currentValue = Math.floor(currentValue);
|
||||
}
|
||||
|
||||
this.element.textContent = currentValue
|
||||
// Update only the text content of the target element
|
||||
targetElement.textContent = currentValue;
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(updateCounter)
|
||||
requestAnimationFrame(updateCounter);
|
||||
} else {
|
||||
this.element.textContent = this.decimalValue && this.targetValue < 10
|
||||
? this.targetValue.toFixed(1)
|
||||
: this.targetValue
|
||||
const finalValue = this.decimalValue && this.targetValue < 10
|
||||
? this.targetValue.toFixed(1)
|
||||
: this.targetValue;
|
||||
targetElement.textContent = finalValue;
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(updateCounter)
|
||||
requestAnimationFrame(updateCounter);
|
||||
}
|
||||
}
|
||||
|
||||
86
app/javascript/controllers/featured_event_controller.js
Executable file
86
app/javascript/controllers/featured_event_controller.js
Executable file
@@ -0,0 +1,86 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["card"]
|
||||
static classes = ["visible"]
|
||||
static values = {
|
||||
threshold: { type: Number, default: 0.1 },
|
||||
rootMargin: { type: String, default: '0px 0px -50px 0px' },
|
||||
staggerDelay: { type: Number, default: 0.2 }
|
||||
}
|
||||
|
||||
connect() {
|
||||
console.log("FeaturedEventController connected")
|
||||
this.setupIntersectionObserver()
|
||||
this.setupStaggeredAnimations()
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
setupIntersectionObserver() {
|
||||
const observerOptions = {
|
||||
threshold: this.thresholdValue,
|
||||
rootMargin: this.rootMarginValue
|
||||
}
|
||||
|
||||
this.observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible')
|
||||
}
|
||||
})
|
||||
}, observerOptions)
|
||||
|
||||
// Observe all card elements within this controller's scope
|
||||
const elements = this.cardTargets
|
||||
console.log("Card targets:", elements)
|
||||
elements.forEach(el => {
|
||||
this.observer.observe(el)
|
||||
})
|
||||
}
|
||||
|
||||
setupStaggeredAnimations() {
|
||||
console.log("Setting up staggered animations")
|
||||
console.log("Card targets:", this.cardTargets)
|
||||
// Add staggered animation delays to cards
|
||||
this.cardTargets.forEach((card, index) => {
|
||||
card.style.transitionDelay = `${index * this.staggerDelayValue}s`
|
||||
card.classList.remove('visible')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Old code
|
||||
<script>
|
||||
// Add animation classes when elements are in view
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px 0px -50px 0px'
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible');
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
// Observe animated elements
|
||||
document.querySelectorAll('.animate-fadeInUp, .animate-slideInLeft, .animate-slideInRight').forEach(el => {
|
||||
observer.observe(el);
|
||||
});
|
||||
|
||||
// Add staggered animation delays
|
||||
document.querySelectorAll('.featured-event-card').forEach((card, index) => {
|
||||
card.style.transitionDelay = `${index * 0.2}s`;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
*/
|
||||
@@ -5,12 +5,15 @@
|
||||
import { application } from "./application"
|
||||
|
||||
import LogoutController from "./logout_controller"
|
||||
import FlashMessage from "./flash_message_controller"
|
||||
import FlashMessageController from "./flash_message_controller"
|
||||
import CounterController from "./counter_controller"
|
||||
import FeaturedEventController from "./featured_event_controller"
|
||||
|
||||
import ShadcnTestController from "./shadcn_test_controller"
|
||||
|
||||
application.register("logout", LogoutController) // Allow logout using js
|
||||
application.register("flash-message", FlashMessage) // Dismiss notification after 5 secondes
|
||||
application.register("flash-message", FlashMessageController) // Dismiss notification after 5 secondes
|
||||
application.register("counter", CounterController) // Simple counter for homepage
|
||||
application.register("featured-event", FeaturedEventController) // Featured event controller for homepage
|
||||
|
||||
application.register("shadcn-test", ShadcnTestController) // Test controller for Shadcn
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["quantity", "cartCount", "cartTotal", "checkoutButton"]
|
||||
static values = { partyId: String }
|
||||
static values = { eventId: String }
|
||||
|
||||
connect() {
|
||||
this.cart = {}
|
||||
@@ -78,7 +78,7 @@ export default class extends Controller {
|
||||
|
||||
const form = document.createElement('form')
|
||||
form.method = 'POST'
|
||||
form.action = `/parties/${this.partyIdValue}/checkout`
|
||||
form.action = `/events/${this.eventIdValue}/checkout`
|
||||
form.style.display = 'none'
|
||||
|
||||
// Add CSRF token
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Party model representing nightlife events and parties
|
||||
# Event model representing nightlife events and events
|
||||
# Manages event details, location data, and publication state
|
||||
class Party < ApplicationRecord
|
||||
# Define states for party lifecycle management
|
||||
# draft: Initial state when party is being created
|
||||
# published: Party is visible to public and can be discovered
|
||||
# canceled: Party has been canceled by organizer
|
||||
# sold_out: Party has reached capacity and tickets are no longer available
|
||||
class Event < ApplicationRecord
|
||||
# Define states for Event lifecycle management
|
||||
# draft: Initial state when Event is being created
|
||||
# published: Event is visible to public and can be discovered
|
||||
# canceled: Event has been canceled by organizer
|
||||
# sold_out: Event has reached capacity and tickets are no longer available
|
||||
enum :state, {
|
||||
draft: 0,
|
||||
published: 1,
|
||||
@@ -18,7 +18,7 @@ class Party < ApplicationRecord
|
||||
has_many :ticket_types, dependent: :destroy
|
||||
has_many :tickets, through: :ticket_types
|
||||
|
||||
# Validations for party attributes
|
||||
# Validations for Event attributes
|
||||
# Basic information
|
||||
validates :name, presence: true, length: { minimum: 3, maximum: 100 }
|
||||
validates :slug, presence: true, length: { minimum: 3, maximum: 100 }
|
||||
@@ -40,12 +40,12 @@ class Party < ApplicationRecord
|
||||
less_than_or_equal_to: 180
|
||||
}
|
||||
|
||||
# Scopes for querying parties with common filters
|
||||
scope :featured, -> { where(featured: true) } # Get featured parties for homepage
|
||||
scope :published, -> { where(state: :published) } # Get publicly visible parties
|
||||
# Scopes for querying events with common filters
|
||||
scope :featured, -> { where(featured: true) } # Get featured events for homepage
|
||||
scope :published, -> { where(state: :published) } # Get publicly visible events
|
||||
scope :search_by_name, ->(query) { where("name ILIKE ?", "%#{query}%") } # Search by name (case-insensitive)
|
||||
|
||||
# Scope for published parties ordered by start time
|
||||
# Scope for published events ordered by start time
|
||||
scope :upcoming, -> { published.where("start_time >= ?", Time.current).order(start_time: :asc) }
|
||||
|
||||
end
|
||||
@@ -2,7 +2,7 @@ class Ticket < ApplicationRecord
|
||||
# Associations
|
||||
belongs_to :user
|
||||
belongs_to :ticket_type
|
||||
has_one :party, through: :ticket_type
|
||||
has_one :event, through: :ticket_type
|
||||
|
||||
# Validations
|
||||
validates :qr_code, presence: true, uniqueness: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class TicketType < ApplicationRecord
|
||||
# Associations
|
||||
belongs_to :party
|
||||
belongs_to :event
|
||||
has_many :tickets, dependent: :destroy
|
||||
|
||||
# Validations
|
||||
@@ -8,12 +8,13 @@ class TicketType < ApplicationRecord
|
||||
validates :description, presence: true, length: { minimum: 10, maximum: 500 }
|
||||
validates :price_cents, presence: true, numericality: { greater_than: 0 }
|
||||
validates :quantity, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
||||
validates :party_id, presence: true
|
||||
validates :sale_start_at, presence: true
|
||||
validates :sale_end_at, presence: true
|
||||
validate :sale_end_after_start
|
||||
validates :requires_id, inclusion: { in: [ true, false ] }
|
||||
validates :minimum_age, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 120 }, allow_nil: true
|
||||
validates :event_id, presence: true
|
||||
|
||||
|
||||
private
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class User < ApplicationRecord
|
||||
:recoverable, :rememberable, :validatable
|
||||
|
||||
# Relationships
|
||||
has_many :parties, dependent: :destroy
|
||||
has_many :events, dependent: :destroy
|
||||
has_many :tickets, dependent: :destroy
|
||||
|
||||
# Validations
|
||||
|
||||
157
app/views/components/_event_finder.html.erb
Executable file
157
app/views/components/_event_finder.html.erb
Executable file
@@ -0,0 +1,157 @@
|
||||
<!-- Event Finder Section -->
|
||||
<section>
|
||||
<div class="container">
|
||||
<div class="event-finder">
|
||||
<div class="finder-header">
|
||||
<h2 class="finder-title">Find Your Perfect Event</h2>
|
||||
<p class="finder-subtitle">Discover afterwork events tailored to your preferences</p>
|
||||
</div>
|
||||
|
||||
<form class="finder-form">
|
||||
<div class="finder-field">
|
||||
<label class="finder-label">
|
||||
<i data-lucide="calendar"></i>
|
||||
Date
|
||||
</label>
|
||||
<input type="date" class="finder-input focus-ring" id="event-date">
|
||||
</div>
|
||||
|
||||
<div class="finder-field">
|
||||
<label class="finder-label">
|
||||
<i data-lucide="map-pin"></i>
|
||||
City
|
||||
</label>
|
||||
<select class="finder-select focus-ring" id="event-city">
|
||||
<option value="">Choose a city</option>
|
||||
<option value="paris">Paris</option>
|
||||
<option value="london">London</option>
|
||||
<option value="berlin">Berlin</option>
|
||||
<option value="madrid">Madrid</option>
|
||||
<option value="barcelona">Barcelona</option>
|
||||
<option value="amsterdam">Amsterdam</option>
|
||||
<option value="rome">Rome</option>
|
||||
<option value="zurich">Zurich</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="finder-field">
|
||||
<label class="finder-label">
|
||||
<i data-lucide="users"></i>
|
||||
Event Type
|
||||
</label>
|
||||
<select class="finder-select focus-ring" id="event-type">
|
||||
<option value="">All types</option>
|
||||
<option value="networking">Networking</option>
|
||||
<option value="tech">Tech & Innovation</option>
|
||||
<option value="creative">Creative & Design</option>
|
||||
<option value="business">Business</option>
|
||||
<option value="startup">Startup</option>
|
||||
<option value="wine">Wine & Tasting</option>
|
||||
<option value="art">Art & Culture</option>
|
||||
<option value="music">Music & Entertainment</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<div class="finder-field price-range">
|
||||
<label class="finder-label">
|
||||
<div class="price-range-label">
|
||||
<span>
|
||||
<i data-lucide="euro"></i>
|
||||
Price Range
|
||||
</span>
|
||||
<span class="price-value" id="price-display">€0 - €100</span>
|
||||
</div>
|
||||
</label>
|
||||
<div style="display: flex; gap: var(--space-3); align-items: center;">
|
||||
<input type="range" class="price-slider" id="price-min" min="0" max="100" value="0" style="flex: 1;">
|
||||
<span style="color: var(--color-neutral-500); font-weight: 600;">to</span>
|
||||
<input type="range" class="price-slider" id="price-max" min="0" max="100" value="100" style="flex: 1;">
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<button type="submit" class="finder-search-btn">
|
||||
<i data-lucide="search"></i>
|
||||
Find Events
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
// Event Finder Functionality
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const priceMin = document.getElementById('price-min');
|
||||
const priceMax = document.getElementById('price-max');
|
||||
const priceDisplay = document.getElementById('price-display');
|
||||
|
||||
if (priceMin && priceMax && priceDisplay) {
|
||||
function updatePriceDisplay() {
|
||||
const minVal = parseInt(priceMin.value);
|
||||
const maxVal = parseInt(priceMax.value);
|
||||
|
||||
// Ensure min doesn't exceed max
|
||||
if (minVal > maxVal) {
|
||||
priceMin.value = maxVal;
|
||||
}
|
||||
|
||||
// Ensure max doesn't go below min
|
||||
if (maxVal < minVal) {
|
||||
priceMax.value = minVal;
|
||||
}
|
||||
|
||||
const finalMin = Math.min(parseInt(priceMin.value), parseInt(priceMax.value));
|
||||
const finalMax = Math.max(parseInt(priceMin.value), parseInt(priceMax.value));
|
||||
|
||||
priceDisplay.textContent = `€${finalMin} - €${finalMax}`;
|
||||
}
|
||||
|
||||
priceMin.addEventListener('input', updatePriceDisplay);
|
||||
priceMax.addEventListener('input', updatePriceDisplay);
|
||||
|
||||
// Set default date to tomorrow
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const dateInput = document.getElementById('event-date');
|
||||
if (dateInput) {
|
||||
dateInput.value = tomorrow.toISOString().split('T')[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Form submission
|
||||
const finderForm = document.querySelector('.finder-form');
|
||||
if (finderForm) {
|
||||
finderForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = {
|
||||
date: document.getElementById('event-date').value,
|
||||
city: document.getElementById('event-city').value,
|
||||
type: document.getElementById('event-type').value,
|
||||
priceMin: priceMin ? priceMin.value : '',
|
||||
priceMax: priceMax ? priceMax.value : ''
|
||||
};
|
||||
|
||||
console.log('Search filters:', formData);
|
||||
|
||||
// Add loading state to button
|
||||
const searchBtn = document.querySelector('.finder-search-btn');
|
||||
if (searchBtn) {
|
||||
const originalText = searchBtn.innerHTML;
|
||||
searchBtn.innerHTML = '<div style="width: 20px; height: 20px; border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%; animation: spin 1s linear infinite;"></div> Searching...';
|
||||
|
||||
// Simulate search
|
||||
setTimeout(() => {
|
||||
searchBtn.innerHTML = originalText;
|
||||
alert('Search completed! Results would be displayed here.');
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,14 +1,14 @@
|
||||
<%= link_to party_path(party.slug, party), class: "group block p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 hover:shadow-md transition-all duration-200" do %>
|
||||
<%= link_to event_path(event.slug, event), class: "group block p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 hover:shadow-md transition-all duration-200" do %>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-16 h-16 bg-slate-200 dark:bg-slate-700 rounded-lg overflow-hidden flex-shrink-0">
|
||||
<%= image_tag party.image, alt: party.name, class: "w-full h-full object-cover" if party.image.present? %>
|
||||
<%= image_tag event.image, alt: event.name, class: "w-full h-full object-cover" if event.image.present? %>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors duration-200">
|
||||
<%= party.name %>
|
||||
<%= event.name %>
|
||||
</h3>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||
<%= l(party.start_time, format: :short) %>
|
||||
<%= l(event.start_time, format: :short) %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,83 +1,41 @@
|
||||
<footer class="py-10 bg-neutral-100 text-neutral-600 border-t border-neutral-200">
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<!-- Column 1: About -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-semibold text-purple-500">À propos</h3>
|
||||
<p class="text-sm text-neutral-600 leading-relaxed">
|
||||
Aperonight est la plateforme qui connecte les amateurs de soirées aux meilleurs événements de leur ville.
|
||||
</p>
|
||||
<div class="flex space-x-4">
|
||||
<a href="#" class="text-neutral-500 hover:text-purple-500 transition-colors">
|
||||
<span class="sr-only">Facebook</span>
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 20.128 22 15.991 22 12z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" class="text-neutral-500 hover:text-purple-500 transition-colors">
|
||||
<span class="sr-only">Instagram</span>
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.024.06 1.378.06 3.808s-.012 2.784-.06 3.808c-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.024.048-1.378.06-3.808.06s-2.784-.012-3.808-.06c-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.048-1.024-.06-1.378-.06-3.808s.012-2.784.06-3.808c.049-1.064.218-1.791.465-2.427A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Column 2: Quick Links -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-semibold text-purple-500">Liens rapides</h3>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li><%= link_to "Accueil", "/", class: "text-neutral-600 hover:text-purple-500 transition-colors" %></li>
|
||||
<li><%= link_to "Événements", "/events", class: "text-neutral-600 hover:text-purple-500 transition-colors" %></li>
|
||||
<li><%= link_to "Organisateurs", "/organizers", class: "text-neutral-600 hover:text-purple-500 transition-colors" %></li>
|
||||
<li><%= link_to "Support", "/support", class: "text-neutral-600 hover:text-purple-500 transition-colors" %></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Column 3: Legal -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-semibold text-purple-500">Légal</h3>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li><%= link_to "Conditions d'utilisation", "/terms", class: "text-neutral-600 hover:text-purple-500 transition-colors" %></li>
|
||||
<li><%= link_to "Politique de confidentialité", "/privacy", class: "text-neutral-600 hover:text-purple-500 transition-colors" %></li>
|
||||
<li><%= link_to "CGV", "/cgv", class: "text-neutral-600 hover:text-purple-500 transition-colors" %></li>
|
||||
<li><%= link_to "Mentions légales", "/legal", class: "text-neutral-600 hover:text-purple-500 transition-colors" %></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Column 4: Contact -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-semibold text-purple-500">Contact</h3>
|
||||
<address class="not-italic text-sm text-neutral-600 space-y-2">
|
||||
<p>
|
||||
<span class="block font-medium">Email:</span>
|
||||
<a href="mailto:hello@aperonight.com" class="text-purple-500 hover:text-purple-400 transition-colors">
|
||||
hello@aperonight.com
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<span class="block font-medium">Support:</span>
|
||||
<a href="mailto:support@aperonight.com" class="text-purple-500 hover:text-purple-400 transition-colors">
|
||||
support@aperonight.com
|
||||
</a>
|
||||
</p>
|
||||
</address>
|
||||
<p class="text-xs text-neutral-500">
|
||||
Réponse sous 24h en semaine
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Bar -->
|
||||
<div class="mt-12 pt-8 border-t border-neutral-200">
|
||||
<div class="flex flex-col md:flex-row justify-between items-center">
|
||||
<p class="text-sm text-neutral-500">
|
||||
© <%= Time.current.year %> Aperonight. Tous droits réservés.
|
||||
</p>
|
||||
<p class="text-xs text-neutral-400 mt-2 md:mt-0">
|
||||
Fait avec 💜 pour la communauté
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<h3>Events</h3>
|
||||
<ul class="footer-links">
|
||||
<li><a href="#">Find Events</a></li>
|
||||
<li><a href="#">Host an Event</a></li>
|
||||
<li><a href="#">Event Categories</a></li>
|
||||
<li><a href="#">Premium Events</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
<div class="footer-section">
|
||||
<h3>Community</h3>
|
||||
<ul class="footer-links">
|
||||
<li><a href="#">Join Us</a></li>
|
||||
<li><a href="#">Member Benefits</a></li>
|
||||
<li><a href="#">Success Stories</a></li>
|
||||
<li><a href="#">Ambassador Program</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h3>Support</h3>
|
||||
<ul class="footer-links">
|
||||
<li><a href="#">Help Center</a></li>
|
||||
<li><a href="#">Contact Us</a></li>
|
||||
<li><a href="#">Safety Guidelines</a></li>
|
||||
<li><a href="#">Cancellation Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h3>Company</h3>
|
||||
<ul class="footer-links">
|
||||
<li><a href="#">About Aperonight</a></li>
|
||||
<li><a href="#">Careers</a></li>
|
||||
<li><a href="#">Press & Media</a></li>
|
||||
<li><a href="#">Partner With Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© 2024 Aperonight. All rights reserved. • <a href="#" style="color: var(--color-accent-400);">Privacy Policy</a> • <a href="#" style="color: var(--color-accent-400);">Terms of Service</a></p>
|
||||
</div>
|
||||
@@ -1,137 +1,137 @@
|
||||
<header class="shadow-sm border-b border-neutral-700">
|
||||
<div class="bg-neutral-900">
|
||||
<nav x-data="{ open: false }" class="bg-neutral-800 border-b border-neutral-700">
|
||||
<!-- Primary Navigation Menu -->
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex">
|
||||
<!-- Logo -->
|
||||
<div class="shrink-0 flex items-center">
|
||||
<%= link_to Rails.application.config.app_name, current_user ? "/dashboard" : "/", class: "text-xl font-bold text-white" %>
|
||||
</div>
|
||||
<!-- Navigation Links -->
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex items-center">
|
||||
<%= link_to t('header.parties'), parties_path,
|
||||
class: "text-neutral-100 hover:text-primary-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
|
||||
%>
|
||||
<%= link_to t('header.concerts'), "#" ,
|
||||
class: "text-neutral-100 hover:text-primary-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
|
||||
%>
|
||||
</div>
|
||||
<header class="shadow-sm border-b border-neutral-700">
|
||||
<div class="bg-neutral-900">
|
||||
<nav x-data="{ open: false }" class="bg-neutral-800 border-b border-neutral-700">
|
||||
<!-- Primary Navigation Menu -->
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex">
|
||||
<!-- Logo -->
|
||||
<div class="shrink-0 flex items-center">
|
||||
<%= link_to Rails.application.config.app_name, current_user ? "/dashboard" : "/", class: "text-xl font-bold text-white" %>
|
||||
</div>
|
||||
<!-- Authentication Links -->
|
||||
<% if user_signed_in? %>
|
||||
<!-- Settings Dropdown -->
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
||||
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
|
||||
<div @click="open = ! open">
|
||||
<button
|
||||
class="bg-primary-700 text-white border border-primary-800 font-medium py-2 px-4 rounded-lg hover:bg-primary-800 transition-colors duration-200 focus-ring">
|
||||
<div>
|
||||
<%= current_user.email.length> 20 ? current_user.email[0,20] + "..." : current_user.email %>
|
||||
</div>
|
||||
<div class="ms-1">
|
||||
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div x-show="open" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75" x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute z-50 mt-2 w-48 rounded-md shadow-lg origin-top-right right-0" style="display: none;"
|
||||
@click="open = false">
|
||||
<div class="rounded-md ring-1 ring-primary-700 py-1 bg-primary-600">
|
||||
<%= link_to t('header.profile') , edit_user_registration_path,
|
||||
class: "block w-full px-4 py-2 text-start text-sm leading-5 text-neutral-100 hover:bg-primary-700" %>
|
||||
<%= link_to t('header.reservations') , "#" ,
|
||||
class: "block w-full px-4 py-2 text-start text-sm leading-5 text-neutral-100 hover:bg-primary-700" %>
|
||||
<%= link_to t('header.logout') , destroy_user_session_path, data: { controller: "logout" ,
|
||||
action: "click->logout#signOut" , logout_url_value: destroy_user_session_path, login_url_value:
|
||||
new_user_session_path, turbo: false },
|
||||
class: "block w-full px-4 py-2 text-start text-sm leading-5 text-neutral-100 hover:bg-primary-700" %>
|
||||
</div>
|
||||
</div>
|
||||
h <!-- Login/Register Links -->
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-6 space-x-4 items-center">
|
||||
<%= link_to t('header.login') , new_user_session_path,
|
||||
class: "text-neutral-100 hover:text-primary-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
|
||||
%>
|
||||
<%= link_to t('header.register') , new_user_registration_path,
|
||||
class: "bg-primary-50 text-primary-600 font-medium py-2 px-4 rounded-lg shadow-sm hover:bg-primary-100 transition-all duration-200"
|
||||
%>
|
||||
</div>
|
||||
<% end %>
|
||||
<!-- Hamburger -->
|
||||
<div class="-me-2 flex items-center sm:hidden">
|
||||
<button @click="open = ! open"
|
||||
class="p-2 rounded-md text-neutral-300 hover:text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
|
||||
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||
<path :class="{ 'hidden': open, 'inline-flex': !open }" class="inline-flex" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
<path :class="{ 'hidden': !open, 'inline-flex': open }" class="hidden" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Navigation Links -->
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex items-center">
|
||||
<%= link_to t('header.parties'), events_path,
|
||||
class: "text-neutral-100 hover:text-primary-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
|
||||
%>
|
||||
<%= link_to t('header.concerts'), "#" ,
|
||||
class: "text-neutral-100 hover:text-primary-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
|
||||
%>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Responsive Navigation Menu -->
|
||||
<div :class="{ 'block': open, 'hidden': !open }" class="hidden sm:hidden">
|
||||
<div class="pt-2 pb-3 space-y-1 bg-primary-600">
|
||||
<%= link_to t('header.parties') , "#" ,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
|
||||
%>
|
||||
<%= link_to t('header.concerts') , "#" ,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
|
||||
%>
|
||||
</div>
|
||||
<!-- Responsive Settings Options -->
|
||||
<div class="pt-4 pb-1 border-t border-primary-700 bg-primary-600">
|
||||
<% if user_signed_in? %>
|
||||
<div class="px-4">
|
||||
<% if current_user.first_name %>
|
||||
<div class="font-medium text-base text-white">
|
||||
<%= current_user.first_name %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="font-medium text-base text-white">
|
||||
<%= current_user.email.length> 20 ? current_user.email[0,20] + "..." : current_user.email %>
|
||||
</div>
|
||||
<%# <div class="font-medium text-sm text-purple-200">
|
||||
<!-- Authentication Links -->
|
||||
<% if user_signed_in? %>
|
||||
<!-- Settings Dropdown -->
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
||||
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
|
||||
<div @click="open = ! open">
|
||||
<button
|
||||
class="bg-primary-700 text-white border border-primary-800 font-medium py-2 px-4 rounded-lg hover:bg-primary-800 transition-colors duration-200 focus-ring">
|
||||
<div>
|
||||
<%= current_user.email.length> 20 ? current_user.email[0,20] + "..." : current_user.email %>
|
||||
</div>
|
||||
<div class="ms-1">
|
||||
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="mt-3 space-y-1">
|
||||
<%= link_to t('header.profile') , edit_user_registration_path,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
|
||||
%>
|
||||
<%= link_to t('header.reservations') , "#" ,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
|
||||
%>
|
||||
|
||||
<%= link_to t('header.logout') , destroy_user_session_path, data: { controller: "logout" , action: "click->logout#signOut",
|
||||
logout_url_value: destroy_user_session_path, login_url_value: new_user_session_path, turbo: false },
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
|
||||
%>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="mt-3 space-y-1">
|
||||
<%= link_to t('header.register') , new_user_registration_path,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
|
||||
%>
|
||||
<div x-show="open" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75" x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute z-50 mt-2 w-48 rounded-md shadow-lg origin-top-right right-0" style="display: none;"
|
||||
@click="open = false">
|
||||
<div class="rounded-md ring-1 ring-primary-700 py-1 bg-primary-600">
|
||||
<%= link_to t('header.profile') , edit_user_registration_path,
|
||||
class: "block w-full px-4 py-2 text-start text-sm leading-5 text-neutral-100 hover:bg-primary-700" %>
|
||||
<%= link_to t('header.reservations') , "#" ,
|
||||
class: "block w-full px-4 py-2 text-start text-sm leading-5 text-neutral-100 hover:bg-primary-700" %>
|
||||
<%= link_to t('header.logout') , destroy_user_session_path, data: { controller: "logout" ,
|
||||
action: "click->logout#signOut" , logout_url_value: destroy_user_session_path, login_url_value:
|
||||
new_user_session_path, turbo: false },
|
||||
class: "block w-full px-4 py-2 text-start text-sm leading-5 text-neutral-100 hover:bg-primary-700" %>
|
||||
</div>
|
||||
</div>
|
||||
h <!-- Login/Register Links -->
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-6 space-x-4 items-center">
|
||||
<%= link_to t('header.login') , new_user_session_path,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
|
||||
%>
|
||||
class: "text-neutral-100 hover:text-primary-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
|
||||
%>
|
||||
<%= link_to t('header.register') , new_user_registration_path,
|
||||
class: "bg-primary-50 text-primary-600 font-medium py-2 px-4 rounded-lg shadow-sm hover:bg-primary-100 transition-all duration-200"
|
||||
%>
|
||||
</div>
|
||||
<% end %>
|
||||
<!-- Hamburger -->
|
||||
<div class="-me-2 flex items-center sm:hidden">
|
||||
<button @click="open = ! open"
|
||||
class="p-2 rounded-md text-neutral-300 hover:text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
|
||||
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||
<path :class="{ 'hidden': open, 'inline-flex': !open }" class="inline-flex" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
<path :class="{ 'hidden': !open, 'inline-flex': open }" class="hidden" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Responsive Navigation Menu -->
|
||||
<div :class="{ 'block': open, 'hidden': !open }" class="hidden sm:hidden">
|
||||
<div class="pt-2 pb-3 space-y-1 bg-primary-600">
|
||||
<%= link_to t('header.parties') , "#" ,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
|
||||
%>
|
||||
<%= link_to t('header.concerts') , "#" ,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
|
||||
%>
|
||||
</div>
|
||||
<!-- Responsive Settings Options -->
|
||||
<div class="pt-4 pb-1 border-t border-primary-700 bg-primary-600">
|
||||
<% if user_signed_in? %>
|
||||
<div class="px-4">
|
||||
<% if current_user.first_name %>
|
||||
<div class="font-medium text-base text-white">
|
||||
<%= current_user.first_name %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="font-medium text-base text-white">
|
||||
<%= current_user.email.length> 20 ? current_user.email[0,20] + "..." : current_user.email %>
|
||||
</div>
|
||||
<%# <div class="font-medium text-sm text-purple-200">
|
||||
<%= current_user.email.length> 20 ? current_user.email[0,20] + "..." : current_user.email %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="mt-3 space-y-1">
|
||||
<%= link_to t('header.profile') , edit_user_registration_path,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
|
||||
%>
|
||||
<%= link_to t('header.reservations') , "#" ,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
|
||||
%>
|
||||
|
||||
<%= link_to t('header.logout') , destroy_user_session_path, data: { controller: "logout" , action: "click->logout#signOut",
|
||||
logout_url_value: destroy_user_session_path, login_url_value: new_user_session_path, turbo: false },
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
|
||||
%>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="mt-3 space-y-1">
|
||||
<%= link_to t('header.register') , new_user_registration_path,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
|
||||
%>
|
||||
<%= link_to t('header.login') , new_user_session_path,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
|
||||
%>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-8">Événements à venir</h1>
|
||||
|
||||
<% if @parties.any? %>
|
||||
<% if @events.any? %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<% @parties.each do |party| %>
|
||||
<% @events.each do |event| %>
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-gray-900"><%= party.name %></h2>
|
||||
<p class="text-sm text-gray-500 mt-1"><%= party.user.email %></p>
|
||||
<h2 class="text-xl font-bold text-gray-900"><%= event.name %></h2>
|
||||
<p class="text-sm text-gray-500 mt-1"><%= event.user.email %></p>
|
||||
</div>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
||||
<%= party.start_time.strftime("%d/%m/%Y") %>
|
||||
<%= event.start_time.strftime("%d/%m/%Y") %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<p class="text-gray-600 text-sm line-clamp-2"><%= party.description.truncate(100) %></p>
|
||||
<p class="text-gray-600 text-sm line-clamp-2"><%= event.description.truncate(100) %></p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-between items-center">
|
||||
<div>
|
||||
<% if party.ticket_types.any? %>
|
||||
<% if event.ticket_types.any? %>
|
||||
<p class="text-sm font-medium text-gray-900">
|
||||
À partir de <%= format_price(party.ticket_types.minimum(:price_cents)) %>€
|
||||
À partir de <%= format_price(event.ticket_types.minimum(:price_cents)) %>€
|
||||
</p>
|
||||
<% else %>
|
||||
<p class="text-sm text-gray-500">Pas de billets disponibles</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= link_to "Voir les détails", party_path(party.slug, party), class: "inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-full shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500" %>
|
||||
<%= link_to "Voir les détails", event_path(event.slug, event), class: "inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-full shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,8 +39,8 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<%# paginate @parties, theme: 'twitter_bootstrap' %>
|
||||
<%= paginate @parties %>
|
||||
<%# paginate @events, theme: 'twitter_bootstrap' %>
|
||||
<%= paginate @events %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-center py-12">
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="min-h-screen bg-neutral-50" data-controller="ticket-cart" data-ticket-cart-party-id-value="<%= params[:id] %>">
|
||||
<div class="min-h-screen bg-neutral-50" data-controller="ticket-cart" data-ticket-cart-event-id-value="<%= params[:id] %>">
|
||||
<div class="max-w-7xl mx-auto md:px-4">
|
||||
|
||||
<nav class="mb-3 text-sm" aria-label="Breadcrumb">
|
||||
@@ -15,7 +15,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
|
||||
<a href="/parties" class="hover:text-primary-600 transition-colors duration-200 mx-2" role="listitem">
|
||||
<a href="/events" class="hover:text-primary-600 transition-colors duration-200 mx-2" role="listitem">
|
||||
Events
|
||||
</a>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</svg>
|
||||
|
||||
<span class="mx-2 font-medium truncate max-w-[150px] sm:max-w-[250px]" role="listitem" aria-current="page">
|
||||
<%= @party.name %>
|
||||
<%= @event.name %>
|
||||
</span>
|
||||
</span>
|
||||
</nav>
|
||||
@@ -32,13 +32,13 @@
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-lg p-4 sm:p-6 md:p-8 mb-6 sm:mb-8">
|
||||
<div class="flex flex-col lg:flex-row gap-6 md:gap-8">
|
||||
<!-- Left Column: Party Info & Image -->
|
||||
<!-- Left Column: Event Info & Image -->
|
||||
<div class="w-full md:w-1/2">
|
||||
<h1 class="text-4xl font-bold text-primary mb-4"><%= @party.name %></h1>
|
||||
<h1 class="text-4xl font-bold text-primary mb-4"><%= @event.name %></h1>
|
||||
|
||||
<% if @party.image.present? %>
|
||||
<% if @event.image.present? %>
|
||||
<div class="relative rounded-2xl overflow-hidden mb-6">
|
||||
<%= image_tag @party.image, class: "w-full h-96 object-cover" %>
|
||||
<%= image_tag @event.image, class: "w-full h-96 object-cover" %>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black to-transparent opacity-50"></div>
|
||||
<div class="absolute bottom-0 left-0 right-0 p-6 bg-gradient-to-t from-black">
|
||||
<h2 class="text-2xl font-semibold text-white mb-2">Event Details</h2>
|
||||
@@ -47,13 +47,13 @@
|
||||
<svg class="w-5 h-5 mr-2 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||
</svg>
|
||||
<span><%= @party.venue_name %></span>
|
||||
<span><%= @event.venue_name %></span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span><%= @party.start_time.strftime("%B %d, %Y at %I:%M %p") %></span>
|
||||
<span><%= @event.start_time.strftime("%B %d, %Y at %I:%M %p") %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-primary mb-2">Description</h2>
|
||||
<p class="text-lg text-slate-600 leading-relaxed"><%= @party.description %></p>
|
||||
<p class="text-lg text-slate-600 leading-relaxed"><%= @event.description %></p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
@@ -73,7 +73,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
<span class="font-medium text-slate-800">Location:</span>
|
||||
<span class="text-slate-600"><%= @party.venue_address %></span>
|
||||
<span class="text-slate-600"><%= @event.venue_address %></span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
@@ -81,7 +81,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
<span class="font-medium text-slate-800">Date:</span>
|
||||
<span class="text-slate-600"><%= @party.start_time.strftime("%B %d, %Y") %></span>
|
||||
<span class="text-slate-600"><%= @event.start_time.strftime("%B %d, %Y") %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,7 +93,7 @@
|
||||
<h2 class="text-2xl font-bold text-slate-800 mb-6">Available Tickets</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<% @party.ticket_types.each do |ticket_type| %>
|
||||
<% @event.ticket_types.each do |ticket_type| %>
|
||||
<% sold_out = ticket_type.quantity <= ticket_type.tickets.count %>
|
||||
<% remaining = ticket_type.quantity - ticket_type.tickets.count %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="h-full bg-neutral-50">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title><%= content_for(:title) || "Aperonight" %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
@@ -8,23 +8,33 @@
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
<%= yield :head %>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Outfit:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Lucide Icons -->
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
|
||||
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
|
||||
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
||||
<link rel="icon" href="/icon.png" type="image/png">
|
||||
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/icon.png">
|
||||
|
||||
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
||||
</head>
|
||||
<body class="h-full font-sans text-neutral-900 antialiased">
|
||||
|
||||
<div class="">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-wrapper">
|
||||
<%= render "components/header" %>
|
||||
|
||||
<main class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<main>
|
||||
<% if flash.any? %>
|
||||
<div class="flash mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="flash py-6">
|
||||
<%= render "shared/flash_messages" %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -34,12 +44,20 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="bg-neutral-100 text-neutral-600">
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<%= render "components/footer" %>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize Lucide icons
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
247
app/views/pages/components.html.erb
Executable file
247
app/views/pages/components.html.erb
Executable file
@@ -0,0 +1,247 @@
|
||||
<div class="py-12">
|
||||
<div class="text-center mb-16">
|
||||
<h1 class="mb-4">Aperonight Design System</h1>
|
||||
<p class="text-xl text-neutral-600 max-w-3xl mx-auto">
|
||||
A comprehensive collection of reusable components for premium event booking experiences.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Buttons Section -->
|
||||
<section class="mb-16">
|
||||
<h2 class="mb-8 text-2xl font-bold">Buttons</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div class="card">
|
||||
<h3 class="demo-title mb-4">Primary Buttons</h3>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button class="btn btn-sm btn-primary">Small</button>
|
||||
<button class="btn btn-md btn-primary">Medium</button>
|
||||
<button class="btn btn-lg btn-primary">Large</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3 class="demo-title mb-4">Secondary Buttons</h3>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button class="btn btn-sm btn-secondary">Small</button>
|
||||
<button class="btn btn-md btn-secondary">Medium</button>
|
||||
<button class="btn btn-lg btn-secondary">Large</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3 class="demo-title mb-4">Outline Buttons</h3>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button class="btn btn-sm btn-outline">Small</button>
|
||||
<button class="btn btn-md btn-outline">Medium</button>
|
||||
<button class="btn btn-lg btn-outline">Large</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3 class="demo-title mb-4">Ghost Buttons</h3>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button class="btn btn-sm btn-ghost">Small</button>
|
||||
<button class="btn btn-md btn-ghost">Medium</button>
|
||||
<button class="btn btn-lg btn-ghost">Large</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Form Elements -->
|
||||
<section class="mb-16">
|
||||
<h2 class="mb-8 text-2xl font-bold">Form Elements</h2>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div class="card">
|
||||
<h3 class="demo-title mb-4">Input Fields</h3>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Event Name</label>
|
||||
<input type="text" class="form-input focus-ring" placeholder="Enter event name">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-textarea focus-ring" rows="3" placeholder="Describe your event"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Category</label>
|
||||
<select class="form-select focus-ring">
|
||||
<option>Select category</option>
|
||||
<option>Networking</option>
|
||||
<option>Tech</option>
|
||||
<option>Creative</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="search-box">
|
||||
<input type="search" class="form-input search-input focus-ring" placeholder="Search events...">
|
||||
<i class="search-icon">
|
||||
<!-- Search icon would be inserted by Lucide JS -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 mt-6">
|
||||
<button type="submit" class="btn btn-md btn-primary">Submit</button>
|
||||
<button type="reset" class="btn btn-md btn-outline">Reset</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3 class="demo-title mb-4">Validation States</h3>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Email (Valid)</label>
|
||||
<input type="email" class="form-input" value="user@example.com" style="border-color: var(--color-success); background: var(--color-success-light);">
|
||||
<div class="form-error mt-1" style="color: var(--color-success); display: flex; align-items: center; gap: 0.25rem;">
|
||||
<!-- Check icon -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||
</svg>
|
||||
Email is valid
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Password (Error)</label>
|
||||
<input type="password" class="form-input" value="123" style="border-color: var(--color-danger); background: var(--color-danger-light);">
|
||||
<div class="form-error">
|
||||
<!-- Alert icon -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
Password must be at least 8 characters
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Phone (Warning)</label>
|
||||
<input type="tel" class="form-input" value="+33 1 23 45" style="border-color: var(--color-warning); background: var(--color-warning-light);">
|
||||
<div class="form-error mt-1" style="color: var(--color-warning-dark); display: flex; align-items: center; gap: 0.25rem;">
|
||||
<!-- Warning icon -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
||||
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||
</svg>
|
||||
Please complete phone number
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Badges and Tags -->
|
||||
<section class="mb-16">
|
||||
<h2 class="mb-8 text-2xl font-bold">Badges & Tags</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div class="card">
|
||||
<h3 class="demo-title mb-4">Event Status Badges</h3>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<span class="badge badge-available">Available</span>
|
||||
<span class="badge badge-limited">Limited</span>
|
||||
<span class="badge badge-sold-out">Sold Out</span>
|
||||
<span class="badge badge-featured">★ Featured</span>
|
||||
<span class="badge badge-vip">VIP</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3 class="demo-title mb-4">Price Tags</h3>
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<div class="price-tag price-tag-sm" style="color: var(--color-neutral-600);">€15</div>
|
||||
<div class="price-tag price-tag-md" style="color: var(--color-primary-600);">€35</div>
|
||||
<div class="price-tag price-tag-lg" style="color: var(--color-accent-600);">€55</div>
|
||||
<div class="price-tag price-tag-md" style="color: var(--color-neutral-600);">
|
||||
<span style="text-decoration: line-through; opacity: 0.7;">€60</span>
|
||||
<span style="color: var(--color-success); margin-left: 0.5rem;">€45</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Cards -->
|
||||
<section class="mb-16">
|
||||
<h2 class="mb-8 text-2xl font-bold">Cards</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<div class="card hover-lift">
|
||||
<div class="card-header">
|
||||
<h3 class="font-bold mb-2">Basic Card</h3>
|
||||
<p class="text-neutral-600 m-0">Simple card with header and content</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>This is a basic card component that can be used for various content types. It includes proper spacing and hover effects.</p>
|
||||
<button class="btn btn-md btn-primary mt-4">Learn More</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="event-card hover-glow">
|
||||
<div style="height: 200px; background: linear-gradient(135deg, var(--color-primary-500), var(--color-accent-500)); display: flex; align-items: center; justify-content: center; color: white; font-size: var(--text-xl); font-weight: bold;">
|
||||
Event Image
|
||||
</div>
|
||||
<div style="padding: var(--space-6);">
|
||||
<div class="flex gap-2 mb-4">
|
||||
<span class="badge badge-featured">★ Featured</span>
|
||||
<span class="badge badge-available">Available</span>
|
||||
</div>
|
||||
<h3 class="font-bold mb-2 text-neutral-900">Networking Event</h3>
|
||||
<p class="text-neutral-600 mb-6 text-sm">Join professionals for an evening of networking and insights.</p>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="price-tag price-tag-md" style="color: var(--color-primary-600);">€35</span>
|
||||
<button class="btn btn-sm btn-primary">Book Now</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3 class="demo-title mb-4">User Profile</h3>
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div class="avatar avatar-lg">
|
||||
<div class="avatar-image" style="background: linear-gradient(135deg, var(--color-primary-200), var(--color-accent-200));"></div>
|
||||
<div class="avatar-status status-online"></div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-bold mb-1">Alex Johnson</h4>
|
||||
<p class="text-neutral-600 text-sm m-0">Senior Developer</p>
|
||||
<div class="rating mt-1">
|
||||
<!-- Stars would be inserted by Lucide JS -->
|
||||
<div class="flex">
|
||||
<svg class="rating-star filled" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
||||
</svg>
|
||||
<svg class="rating-star filled" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
||||
</svg>
|
||||
<svg class="rating-star filled" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
||||
</svg>
|
||||
<svg class="rating-star filled" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
||||
</svg>
|
||||
<svg class="rating-star empty" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm text-neutral-600 ml-2">4.2</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-sm btn-primary flex-1">Connect</button>
|
||||
<button class="btn btn-sm btn-outline">Message</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -4,7 +4,7 @@
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-slate-100 mb-6">Tableau de bord</h1>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
|
||||
<%= render partial: 'components/metric_card', locals: { title: "Événements disponibles", value: @available_parties, classes: "from-purple-100 to-indigo-100" } %>
|
||||
<%= render partial: 'components/metric_card', locals: { title: "Événements disponibles", value: @available_events, classes: "from-purple-100 to-indigo-100" } %>
|
||||
|
||||
<%= render partial: 'components/metric_card', locals: { title: "Événements aujourd'hui", value: @events_this_week, classes: "from-purple-100 to-indigo-100" } %>
|
||||
|
||||
@@ -13,17 +13,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Today's parties -->
|
||||
<!-- Today's events -->
|
||||
<div class="card hover-lift mb-8">
|
||||
<div class="card-header">
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Évenements du jour</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if @today_parties.any? %>
|
||||
<% if @today_events.any? %>
|
||||
<ul class="space-y-4">
|
||||
<% @today_parties.each do |party| %>
|
||||
<% @today_events.each do |event| %>
|
||||
<li>
|
||||
<%= render partial: 'components/party_item', locals: { party: party } %>
|
||||
<%= render partial: 'components/event_item', locals: { event: event } %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
@@ -33,17 +33,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tomorrow's parties -->
|
||||
<!-- Tomorrow's events -->
|
||||
<div class="card hover-lift mb-8">
|
||||
<div class="card-header">
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Évenements de demain</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if @tomorrow_parties.any? %>
|
||||
<% if @tomorrow_events.any? %>
|
||||
<ul class="space-y-4">
|
||||
<% @tomorrow_parties.each do |party| %>
|
||||
<% @tomorrow_events.each do |event| %>
|
||||
<li>
|
||||
<%= render partial: 'components/party_item', locals: { party: party } %>
|
||||
<%= render partial: 'components/event_item', locals: { event: event } %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
@@ -53,24 +53,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Other upcoming parties with pagination -->
|
||||
<!-- Other upcoming events with pagination -->
|
||||
<div class="card hover-lift">
|
||||
<div class="card-header">
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Autres évenements à venir</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if @other_parties.any? %>
|
||||
<% if @other_events.any? %>
|
||||
<ul class="space-y-4">
|
||||
<% @other_parties.each do |party| %>
|
||||
<% @other_events.each do |event| %>
|
||||
<li>
|
||||
<%= render partial: 'components/party_item', locals: { party: party } %>
|
||||
<%= render partial: 'components/event_item', locals: { event: event } %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="mt-8">
|
||||
<%= paginate @other_parties %>
|
||||
<%= paginate @other_events %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-slate-600 dark:text-slate-400">Aucune autre partie à venir.</p>
|
||||
|
||||
@@ -1,46 +1,217 @@
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-3xl font-bold mb-8">Upcoming Events</h1>
|
||||
|
||||
<% if @parties.any? %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<% @parties.each do |party| %>
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<% if party.image.present? %>
|
||||
<img src="<%= party.image %>" alt="<%= party.name %>" class="w-full h-48 object-cover">
|
||||
<% else %>
|
||||
<div class="bg-gray-200 border-2 border-dashed rounded-xl w-full h-48 flex items-center justify-center">
|
||||
<span class="text-gray-500">No Image</span>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-semibold mb-2"><%= party.name %></h2>
|
||||
<p class="text-gray-600 mb-4"><%= party.description.truncate(100) %></p>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500"><%= party.venue_name %></p>
|
||||
<p class="text-sm text-gray-500"><%= party.venue_address %></p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
|
||||
<%= party.state.humanize %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% content_for :title, "All Events - Aperonight" %>
|
||||
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h1 class="section-title">All Events</h1>
|
||||
<p class="section-description">Discover all our upcoming afterwork events tailored to your interests.</p>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="mt-8 flex justify-center">
|
||||
<%= paginate @parties %>
|
||||
<div class="events-filter-bar" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-8); flex-wrap: wrap; gap: var(--space-4);">
|
||||
<div class="search-box" style="flex: 1; max-width: 400px;">
|
||||
<input type="text" class="form-input search-input" placeholder="Search events...">
|
||||
<i data-lucide="search" class="search-icon"></i>
|
||||
</div>
|
||||
<div style="display: flex; gap: var(--space-3);">
|
||||
<select class="form-select" style="min-width: 150px;">
|
||||
<option>All Categories</option>
|
||||
<option>Networking</option>
|
||||
<option>Tech & Innovation</option>
|
||||
<option>Creative & Design</option>
|
||||
<option>Business</option>
|
||||
<option>Wine & Tasting</option>
|
||||
</select>
|
||||
<select class="form-select" style="min-width: 120px;">
|
||||
<option>Any Price</option>
|
||||
<option>Free</option>
|
||||
<option>Under €30</option>
|
||||
<option>€30 - €60</option>
|
||||
<option>Over €60</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-center py-12">
|
||||
<h2 class="text-xl font-medium text-gray-900 mb-4">No events found</h2>
|
||||
<p class="text-gray-500">Check back later for upcoming events.</p>
|
||||
|
||||
<div class="events-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: var(--space-6);">
|
||||
<!-- Event 1 -->
|
||||
<div class="event-card hover-glow">
|
||||
<img src="https://images.unsplash.com/photo-1540039155733-5bb30b53aa14?w=400&h=200&fit=crop" alt="Tech Networking Night" style="width: 100%; height: 200px; object-fit: cover;">
|
||||
<div style="padding: var(--space-5);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--space-3);">
|
||||
<h3 style="font-family: var(--font-display); font-size: var(--text-lg); font-weight: 700; margin: 0;">Tech & Innovation Networking Night</h3>
|
||||
<span class="badge badge-featured">★ Featured</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3); color: var(--color-neutral-600); font-size: var(--text-sm);">
|
||||
<i data-lucide="calendar" style="width: 16px; height: 16px;"></i>
|
||||
<span>Thu, Mar 15 • 18:30</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-4); color: var(--color-neutral-600); font-size: var(--text-sm);">
|
||||
<i data-lucide="map-pin" style="width: 16px; height: 16px;"></i>
|
||||
<span>Le Perchoir Marais, Paris</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span class="price-tag price-tag-md">€35</span>
|
||||
<button class="btn btn-sm btn-primary">Reserve Spot</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event 2 -->
|
||||
<div class="event-card hover-glow">
|
||||
<img src="https://images.unsplash.com/photo-1574391884720-bbc3740c59d1?w=400&h=200&fit=crop" alt="Creative Afterwork" style="width: 100%; height: 200px; object-fit: cover;">
|
||||
<div style="padding: var(--space-5);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--space-3);">
|
||||
<h3 style="font-family: var(--font-display); font-size: var(--text-lg); font-weight: 700; margin: 0;">Creative Directors Exclusive Meetup</h3>
|
||||
<span class="badge badge-vip">VIP</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3); color: var(--color-neutral-600); font-size: var(--text-sm);">
|
||||
<i data-lucide="calendar" style="width: 16px; height: 16px;"></i>
|
||||
<span>Fri, Mar 16 • 19:00</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-4); color: var(--color-neutral-600); font-size: var(--text-sm);">
|
||||
<i data-lucide="map-pin" style="width: 16px; height: 16px;"></i>
|
||||
<span>Atelier Des Lumières, Paris</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span class="price-tag price-tag-md">€65</span>
|
||||
<button class="btn btn-sm btn-primary">Join VIP</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event 3 -->
|
||||
<div class="event-card hover-glow">
|
||||
<img src="https://images.unsplash.com/photo-1569949381669-ecf31ae8e613?w=400&h=200&fit=crop" alt="Wine Tasting" style="width: 100%; height: 200px; object-fit: cover;">
|
||||
<div style="padding: var(--space-5);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--space-3);">
|
||||
<h3 style="font-family: var(--font-display); font-size: var(--text-lg); font-weight: 700; margin: 0;">Wine & Business Premium Tasting</h3>
|
||||
<span class="badge badge-available">Available</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3); color: var(--color-neutral-600); font-size: var(--text-sm);">
|
||||
<i data-lucide="calendar" style="width: 16px; height: 16px;"></i>
|
||||
<span>Sat, Mar 18 • 17:00</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-4); color: var(--color-neutral-600); font-size: var(--text-sm);">
|
||||
<i data-lucide="map-pin" style="width: 16px; height: 16px;"></i>
|
||||
<span>Cave Legrand, Paris</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span class="price-tag price-tag-md">€55</span>
|
||||
<button class="btn btn-sm btn-secondary">Book Now</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event 4 -->
|
||||
<div class="event-card hover-glow">
|
||||
<img src="https://images.unsplash.com/photo-1511795409834-ef04bbd61622?w=400&h=200&fit=crop" alt="Startup Pitch Night" style="width: 100%; height: 200px; object-fit: cover;">
|
||||
<div style="padding: var(--space-5);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--space-3);">
|
||||
<h3 style="font-family: var(--font-display); font-size: var(--text-lg); font-weight: 700; margin: 0;">Startup Pitch Night</h3>
|
||||
<span class="badge badge-limited">Limited</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3); color: var(--color-neutral-600); font-size: var(--text-sm);">
|
||||
<i data-lucide="calendar" style="width: 16px; height: 16px;"></i>
|
||||
<span>Mon, Mar 20 • 18:00</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-4); color: var(--color-neutral-600); font-size: var(--text-sm);">
|
||||
<i data-lucide="map-pin" style="width: 16px; height: 16px;"></i>
|
||||
<span>Station F, Paris</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span class="price-tag price-tag-md">€25</span>
|
||||
<button class="btn btn-sm btn-primary">Reserve Spot</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event 5 -->
|
||||
<div class="event-card hover-glow">
|
||||
<img src="https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=400&h=200&fit=crop" alt="Jazz & Cocktails" style="width: 100%; height: 200px; object-fit: cover;">
|
||||
<div style="padding: var(--space-5);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--space-3);">
|
||||
<h3 style="font-family: var(--font-display); font-size: var(--text-lg); font-weight: 700; margin: 0;">Jazz & Cocktails Evening</h3>
|
||||
<span class="badge badge-available">Available</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3); color: var(--color-neutral-600); font-size: var(--text-sm);">
|
||||
<i data-lucide="calendar" style="width: 16px; height: 16px;"></i>
|
||||
<span>Wed, Mar 22 • 20:00</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-4); color: var(--color-neutral-600); font-size: var(--text-sm);">
|
||||
<i data-lucide="map-pin" style="width: 16px; height: 16px;"></i>
|
||||
<span>Blue Note, Paris</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span class="price-tag price-tag-md">€40</span>
|
||||
<button class="btn btn-sm btn-secondary">Book Now</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event 6 -->
|
||||
<div class="event-card hover-glow">
|
||||
<img src="https://images.unsplash.com/photo-1511795409834-ef04bbd61622?w=400&h=200&fit=crop" alt="Art & Wine" style="width: 100%; height: 200px; object-fit: cover;">
|
||||
<div style="padding: var(--space-5);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--space-3);">
|
||||
<h3 style="font-family: var(--font-display); font-size: var(--text-lg); font-weight: 700; margin: 0;">Art & Wine Gallery Night</h3>
|
||||
<span class="badge badge-featured">★ Featured</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3); color: var(--color-neutral-600); font-size: var(--text-sm);">
|
||||
<i data-lucide="calendar" style="width: 16px; height: 16px;"></i>
|
||||
<span>Fri, Mar 24 • 19:30</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-4); color: var(--color-neutral-600); font-size: var(--text-sm);">
|
||||
<i data-lucide="map-pin" style="width: 16px; height: 16px;"></i>
|
||||
<span>Gallery Lafayette, Paris</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span class="price-tag price-tag-md">€30</span>
|
||||
<button class="btn btn-sm btn-primary">Reserve Spot</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: center; margin-top: var(--space-8);">
|
||||
<nav style="display: flex; gap: var(--space-2);">
|
||||
<a href="#" class="btn btn-outline" style="padding: var(--space-2) var(--space-4);">Previous</a>
|
||||
<a href="#" class="btn btn-primary" style="padding: var(--space-2) var(--space-4);">1</a>
|
||||
<a href="#" class="btn btn-outline" style="padding: var(--space-2) var(--space-4);">2</a>
|
||||
<a href="#" class="btn btn-outline" style="padding: var(--space-2) var(--space-4);">3</a>
|
||||
<a href="#" class="btn btn-outline" style="padding: var(--space-2) var(--space-4);">Next</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.events-filter-bar .search-input {
|
||||
padding-left: var(--space-12);
|
||||
}
|
||||
|
||||
.events-filter-bar .search-icon {
|
||||
position: absolute;
|
||||
left: var(--space-4);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-neutral-400);
|
||||
}
|
||||
|
||||
.events-filter-bar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.events-filter-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.events-filter-bar .search-box {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.events-filter-bar > div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,250 +1,211 @@
|
||||
<% content_for :title, "Aperonight - Découvrez des événements après-travail de luxe" %>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="relative bg-neutral-50 min-h-[70vh] flex items-center">
|
||||
<div class="absolute inset-0 bg-white bg-opacity-60"></div>
|
||||
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 class="text-5xl md:text-7xl font-bold text-neutral-900 mb-6 leading-tight">
|
||||
Découvrez les afterworks et soirée
|
||||
<span class="text-transparent bg-clip-text bg-gradient-to-r from-purple-600 to-pink-600">
|
||||
à Paris
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-xl md:text-2xl text-neutral-700 mb-8 max-w-3xl mx-auto leading-relaxed">
|
||||
Les meilleures soirées, concerts et afterworks de Paris en un clic. Réservez vos places et vivez des expériences uniques.
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
<%= link_to "Explorer les soirées", parties_path, class: "bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white font-semibold py-4 px-8 rounded-full transition-all duration-300 transform hover:scale-105 shadow-lg" %>
|
||||
<%= link_to "Voir les concerts", "#", class: "bg-white border border-neutral-300 hover:border-purple-300 text-neutral-700 font-semibold py-4 px-8 rounded-full transition-all duration-300" %>
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<div class="hero-content">
|
||||
<h1>Découvrez les afterworks à Paris</h1>
|
||||
<p class="subtitle">Connectez-vous avec des professionnels, explorez des lieux uniques et créez des expériences mémorables lors d'événements après-travail soigneusement sélectionnés dans votre ville.</p>
|
||||
|
||||
<div class="cta-group">
|
||||
<%= link_to "Explorer les événements", events_path, class: "btn btn-lg btn-primary" %>
|
||||
<%= link_to "Organiser un événement", "#", class: "btn btn-lg btn-secondary" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Metrics -->
|
||||
<section class="bg-neutral-50 py-20">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-4xl md:text-5xl font-bold text-neutral-900 mb-4">
|
||||
Des chiffres qui parlent
|
||||
</h2>
|
||||
<p class="text-xl text-neutral-600 max-w-2xl mx-auto">
|
||||
La plateforme préférée des Parisiens pour vivre la nuit
|
||||
</p>
|
||||
|
||||
<%= render "components/event_finder" %>
|
||||
|
||||
<!-- Featured Events Section -->
|
||||
<section class="section featured-events" id="events">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">En vedette cette semaine</h2>
|
||||
<p class="section-description">Événements de luxe sélectionnés avec soin qui réunissent les meilleurs professionnels et créateurs de la ville.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12">
|
||||
<!-- Total Events -->
|
||||
<div class="group relative">
|
||||
<div class="relative overflow-hidden rounded-2xl bg-white border border-neutral-200 hover:border-purple-300 transition-all duration-300 p-8">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-purple-100 to-indigo-100 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div class="relative">
|
||||
<div class="text-5xl md:text-6xl font-light bg-gradient-to-r from-purple-600 via-indigo-600 to-pink-600 bg-clip-text text-transparent mb-3">
|
||||
<span class="counter" data-controller="counter" data-counter-target-value="127">0</span>
|
||||
|
||||
<div class="featured-events-grid" data-controller="featured-event">
|
||||
<!-- Featured Event 1 -->
|
||||
<div class="featured-event-card" data-featured-event-target="card">
|
||||
<img src="https://images.unsplash.com/photo-1540039155733-5bb30b53aa14?w=600&h=300&fit=crop" alt="Soirée de réseautage Tech & Innovation" class="featured-event-image" data-featured-event-target="animated">
|
||||
<div class="featured-event-content">
|
||||
<div class="featured-event-badges">
|
||||
<span class="badge badge-featured">★ En vedette</span>
|
||||
<span class="badge badge-available">Disponible</span>
|
||||
</div>
|
||||
<h3 class="featured-event-title">Soirée de réseautage Tech & Innovation</h3>
|
||||
<div class="featured-event-meta">
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="calendar"></i>
|
||||
Jeu, Mar 15 • 18:30 - 22:00
|
||||
</div>
|
||||
<p class="text-neutral-700 font-mono uppercase tracking-widest text-sm font-medium">
|
||||
Événements organisés
|
||||
</p>
|
||||
<div class="mt-4 h-1 bg-gradient-to-r from-purple-500 via-indigo-500 to-pink-500 rounded-full w-0 group-hover:w-full transition-all duration-500"></div>
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="map-pin"></i>
|
||||
Le Perchoir Marais, Paris
|
||||
</div>
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="users"></i>
|
||||
85 participants • 15 places disponibles
|
||||
</div>
|
||||
</div>
|
||||
<p class="featured-event-description">Rejoignez plus de 100 professionnels de la technologie pour une soirée exclusive de réseautage, de boissons et de découvertes des dernières tendances innovantes. Connectez-vous avec des startups, des investisseurs et des leaders de l'industrie.</p>
|
||||
<div class="featured-event-footer">
|
||||
<span class="featured-event-price">€35</span>
|
||||
<button class="btn btn-sm btn-primary">Réserver une place</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Users -->
|
||||
<div class="group relative">
|
||||
<div class="relative overflow-hidden rounded-2xl bg-white border border-neutral-200 hover:border-purple-300 transition-all duration-300 p-8">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-purple-100 to-indigo-100 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div class="relative">
|
||||
<div class="text-5xl md:text-6xl font-light bg-gradient-to-r from-purple-600 via-indigo-600 to-pink-600 bg-clip-text text-transparent mb-3">
|
||||
<span class="counter" data-controller="counter" data-counter-target-value="1433">0</span>+
|
||||
|
||||
<!-- Featured Event 2 -->
|
||||
<div class="featured-event-card" data-featured-event-target="card">
|
||||
<img src="https://images.unsplash.com/photo-1574391884720-bbc3740c59d1?w=400&h=240&fit=crop" alt="Rencontre exclusive des directeurs créatifs" class="featured-event-image" data-featured-event-target="animated">
|
||||
<div class="featured-event-content">
|
||||
<div class="featured-event-badges">
|
||||
<span class="badge badge-vip">VIP</span>
|
||||
<span class="badge badge-limited">Limité</span>
|
||||
</div>
|
||||
<h3 class="featured-event-title">Rencontre exclusive des directeurs créatifs</h3>
|
||||
<div class="featured-event-meta">
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="calendar"></i>
|
||||
Ven, Mar 16 • 19:00 - 23:00
|
||||
</div>
|
||||
<p class="text-neutral-700 font-mono uppercase tracking-widest text-sm font-medium">
|
||||
Membres actifs
|
||||
</p>
|
||||
<div class="mt-4 h-1 bg-gradient-to-r from-purple-500 via-indigo-500 to-pink-500 rounded-full w-0 group-hover:w-full transition-all duration-500"></div>
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="map-pin"></i>
|
||||
Atelier Des Lumières, Paris
|
||||
</div>
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="users"></i>
|
||||
30 créatifs • 8 places disponibles
|
||||
</div>
|
||||
</div>
|
||||
<p class="featured-event-description">Un rassemblement intime de directeurs créatifs, de designers et d'artistes visuels. Découvrez des installations d'art immersives tout en vous connectant avec des pionniers de l'industrie.</p>
|
||||
<div class="featured-event-footer">
|
||||
<span class="featured-event-price">€65</span>
|
||||
<button class="btn btn-sm btn-primary">Rejoindre le VIP</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Average Rating -->
|
||||
<div class="group relative">
|
||||
<div class="relative overflow-hidden rounded-2xl bg-white border border-neutral-200 hover:border-purple-300 transition-all duration-300 p-8">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-purple-100 to-indigo-100 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div class="relative">
|
||||
<div class="text-5xl md:text-6xl font-light bg-gradient-to-r from-purple-600 via-indigo-600 to-pink-600 bg-clip-text text-transparent mb-3">
|
||||
<span class="counter" data-controller="counter" data-counter-target-value="4.4" data-counter-decimal-value="true">0</span>/5
|
||||
|
||||
<!-- Featured Event 3 -->
|
||||
<div class="featured-event-card" data-featured-event-target="card">
|
||||
<img src="https://images.unsplash.com/photo-1569949381669-ecf31ae8e613?w=400&h=240&fit=crop" alt="Dégustation de vin et d'affaires de luxe" class="featured-event-image" data-featured-event-target="animated">
|
||||
<div class="featured-event-content">
|
||||
<div class="featured-event-badges">
|
||||
<span class="badge badge-available">Disponible</span>
|
||||
</div>
|
||||
<h3 class="featured-event-title">Dégustation de vin et d'affaires de luxe</h3>
|
||||
<div class="featured-event-meta">
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="calendar"></i>
|
||||
Sam, Mar 18 • 17:00 - 21:00
|
||||
</div>
|
||||
<p class="text-neutral-700 font-mono uppercase tracking-widest text-sm font-medium">
|
||||
Note moyenne des soirées
|
||||
</p>
|
||||
<div class="mt-4 h-1 bg-gradient-to-r from-purple-500 via-indigo-500 to-pink-500 rounded-full w-0 group-hover:w-full transition-all duration-500"></div>
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="map-pin"></i>
|
||||
Cave Legrand, Paris
|
||||
</div>
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="users"></i>
|
||||
45 professionnels • 12 places disponibles
|
||||
</div>
|
||||
</div>
|
||||
<p class="featured-event-description">Découvrez des vins français exceptionnels tout en vous connectant avec des professionnels d'affaires. Guidance d'un sommelier expert et dégustations de luxe dans une cave à vin historique.</p>
|
||||
<div class="featured-event-footer">
|
||||
<span class="featured-event-price">€55</span>
|
||||
<button class="btn btn-sm btn-secondary">Réserver maintenant</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: var(--space-12);">
|
||||
<%= link_to "Voir tous les événements", events_path, class: "btn btn-lg btn-outline" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Additional Stats Row -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 mt-12">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-purple-600">
|
||||
<span class="counter" data-controller="counter" data-counter-target-value="79">0</span>%
|
||||
<!-- Features Section -->
|
||||
<section class="section features-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Why Choose Aperonight?</h2>
|
||||
<p class="section-description">We curate premium experiences that connect professionals and create lasting relationships.</p>
|
||||
</div>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i data-lucide="crown"></i>
|
||||
</div>
|
||||
<p class="text-neutral-600 text-sm font-mono uppercase tracking-wide font-medium">Taux de remplissage</p>
|
||||
<h3 class="feature-title">Premium Curation</h3>
|
||||
<p class="feature-description">Every event is carefully selected and designed to provide exceptional value and networking opportunities.</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-purple-600">
|
||||
<span class="counter" data-controller="counter" data-counter-target-value="12">0</span>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i data-lucide="shield-check"></i>
|
||||
</div>
|
||||
<p class="text-neutral-600 text-sm font-mono uppercase tracking-wide font-medium">Arrondissements</p>
|
||||
<h3 class="feature-title">Secure & Trusted</h3>
|
||||
<p class="feature-description">Safe payments, verified venues, and trusted community with comprehensive insurance coverage.</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-purple-600">
|
||||
<span class="counter" data-controller="counter" data-counter-target-value="156">0</span>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i data-lucide="users-2"></i>
|
||||
</div>
|
||||
<p class="text-neutral-600 text-sm font-mono uppercase tracking-wide font-medium">Établissements partenaires</p>
|
||||
<h3 class="feature-title">Quality Networking</h3>
|
||||
<p class="feature-description">Connect with verified professionals, entrepreneurs, and industry leaders in intimate settings.</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-purple-600">
|
||||
<span class="counter" data-controller="counter" data-counter-target-value="98">0</span>%
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i data-lucide="zap"></i>
|
||||
</div>
|
||||
<p class="text-neutral-600 text-sm font-mono uppercase tracking-wide font-medium">Satisfaction client</p>
|
||||
<h3 class="feature-title">Instant Booking</h3>
|
||||
<p class="feature-description">Seamless reservation process with instant confirmation and easy event management.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Quick Search Section -->
|
||||
<section id="search" class="bg-white py-16">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-gradient-to-br from-neutral-50 to-white border border-neutral-200 rounded-2xl p-8 shadow-lg">
|
||||
<h2 class="text-3xl font-bold text-neutral-900 text-center mb-8">Trouvez votre prochaine soirée</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 items-end">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-neutral-700 mb-2">Quand ?</label>
|
||||
<input type="date" class="w-full bg-white border border-neutral-300 text-neutral-900 rounded-lg p-3 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all" placeholder="Choisir une date">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-neutral-700 mb-2">Type d'événement</label>
|
||||
<select class="w-full bg-white border border-neutral-300 text-neutral-900 rounded-lg p-3 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all">
|
||||
<option value="">Tous les types</option>
|
||||
<option value="club">Soirées club</option>
|
||||
<option value="afterwork">Afterworks</option>
|
||||
<option value="concert">Concerts</option>
|
||||
<option value="vip">Événements VIP</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-neutral-700 mb-2">Genre musical</label>
|
||||
<select class="w-full bg-white border border-neutral-300 text-neutral-900 rounded-lg p-3 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all">
|
||||
<option value="">Tous les genres</option>
|
||||
<option value="house">House/Techno</option>
|
||||
<option value="hiphop">Hip-Hop</option>
|
||||
<option value="pop">Pop</option>
|
||||
<option value="rock">Rock</option>
|
||||
<option value="electro">Électro</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button class="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white font-semibold py-3 px-6 rounded-lg transition-all duration-300 transform hover:scale-105">
|
||||
Rechercher
|
||||
</button>
|
||||
<!-- Stats Section -->
|
||||
<section class="section stats-section">
|
||||
<div class="container">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item" data-controller="counter" data-action="counter:scroll->counter#animate">
|
||||
<span class="stat-number" data-target-value="150">0</span>
|
||||
<div class="stat-label">Monthly Events</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Events -->
|
||||
<section id="events" class="bg-neutral-50 py-20">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-4xl font-bold text-neutral-900 mb-4">Événements du moment</h2>
|
||||
<p class="text-xl text-neutral-600">Les soirées et concerts les plus populaires cette semaine</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<% @parties.each do |party| %>
|
||||
<div class="bg-white border border-neutral-200 rounded-2xl overflow-hidden hover:transform hover:scale-105 transition-all duration-300 shadow-lg">
|
||||
<div class="h-56 bg-gradient-to-br from-purple-500 via-pink-500 to-red-500 relative"
|
||||
style="background-image: url('<%= party.image || "https://images.unsplash.com/photo-1506157786151-b84b9d3d78d8?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80" %>');
|
||||
background-size: cover;
|
||||
background-position: center;">
|
||||
<div class="absolute top-4 right-4 bg-white bg-opacity-90 text-neutral-900 px-3 py-1 rounded-full text-sm font-medium">
|
||||
<%= party.venue_name.split(' ').first %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<h3 class="text-2xl font-bold text-neutral-900"><%= party.name.upcase %></h3>
|
||||
<% if party.ticket_types.any? %>
|
||||
<span class="text-purple-600 font-semibold"><%= number_to_currency(party.ticket_types.first.price_cents / 100.0, unit: "€", separator: ",", delimiter: " ") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-neutral-600 mb-2"><%= party.venue_name %>, <%= party.venue_address.split(',').first %></p>
|
||||
<p class="text-neutral-700 mb-4"><%= I18n.l(party.start_time, format: "%A %Hh") if party.start_time %> • <%= party.description.split('.').first %></p>
|
||||
<%= link_to "Voir les détails", party_path(party.slug, party), class: "w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white font-semibold py-3 rounded-lg transition-all duration-300 text-center block" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-12">
|
||||
<%= link_to "Voir tous les événements →", parties_path, class: "text-purple-600 hover:text-purple-700 text-lg font-medium transition-all duration-300 border-b-2 border-purple-600 hover:border-purple-700" %>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features -->
|
||||
<section class="bg-white py-20">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-4xl font-bold text-neutral-900 mb-4">Pourquoi choisir <%= Rails.application.config.app_name %> ?</h2>
|
||||
<p class="text-xl text-neutral-600 max-w-2xl mx-auto">La plateforme préférée des Parisiens pour sortir</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div class="text-center p-8">
|
||||
<div class="bg-gradient-to-r from-purple-600 to-pink-600 rounded-full w-20 h-20 flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-neutral-900 mb-3">Découverte facile</h3>
|
||||
<p class="text-neutral-600 leading-relaxed">Trouvez les meilleures soirées et concerts de Paris en quelques clics grâce à notre algorithme personnalisé</p>
|
||||
<div class="stat-item" data-controller="counter" data-action="counter:scroll->counter#animate">
|
||||
<span class="stat-number" data-target-value="5200">0</span>
|
||||
<div class="stat-label">Active Members</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center p-8">
|
||||
<div class="bg-gradient-to-r from-purple-600 to-pink-600 rounded-full w-20 h-20 flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-neutral-900 mb-3">Réservation sécurisée</h3>
|
||||
<p class="text-neutral-600 leading-relaxed">Paiement 100% sécurisé et billets électroniques avec QR code sur votre mobile</p>
|
||||
<div class="stat-item" data-controller="counter" data-action="counter:scroll->counter#animate">
|
||||
<span class="stat-number" data-target-value="200">0</span>
|
||||
<div class="stat-label">Partner Venues</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center p-8">
|
||||
<div class="bg-gradient-to-r from-purple-600 to-pink-600 rounded-full w-20 h-20 flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-neutral-900 mb-3">Accès rapide</h3>
|
||||
<p class="text-neutral-600 leading-relaxed">Entrée express avec validation mobile de vos billets. Plus besoin d'imprimer !</p>
|
||||
<div class="stat-item" data-controller="counter" data-action="counter:scroll->counter#animate">
|
||||
<span class="stat-number" data-target-value="98">0</span>
|
||||
<div class="stat-label">Satisfaction Rate</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="bg-gradient-to-r from-purple-100 via-pink-50 to-indigo-100 py-20">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h2 class="text-4xl font-bold text-neutral-900 mb-6">Prêt à vivre la nuit parisienne ?</h2>
|
||||
<p class="text-xl text-neutral-700 mb-8">Rejoignez des milliers de party-goers qui utilisent Aperonight chaque semaine</p>
|
||||
<%= link_to new_user_registration_path, class: "bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white font-semibold py-4 px-8 rounded-full text-lg transition-all duration-300 transform hover:scale-105 shadow-xl" do %>
|
||||
S'inscrire gratuitement
|
||||
<% end %>
|
||||
<section class="cta-section">
|
||||
<div class="container">
|
||||
<div class="cta-content">
|
||||
<h2>Ready to Join the Community?</h2>
|
||||
<p>Start discovering amazing events and connect with like-minded professionals in your city.</p>
|
||||
<div style="display: flex; gap: var(--space-4); justify-content: center; flex-wrap: wrap;">
|
||||
<button class="btn btn-lg" style="background: white; color: var(--color-primary-600); border: 2px solid white;">
|
||||
<i data-lucide="user-plus"></i>
|
||||
Join Now - Free
|
||||
</button>
|
||||
<button class="btn btn-lg btn-ghost" style="border: 2px solid rgba(255,255,255,0.5); color: white;">
|
||||
<i data-lucide="calendar"></i>
|
||||
Browse Events
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
Reference in New Issue
Block a user