Files
blackcanyontickets/src/pages/calendar.astro
dzinesco 6746fc72b7 fix: correct fee settings button link and improve calendar theming
- Fix fee settings button in dashboard to link to /settings/fees instead of /calendar
- Implement proper theme management system for calendar page
- Add theme background handler and data-theme-background attribute
- Replace broken theme import with complete theme management
- Both dashboard and calendar now properly support light/dark themes
- Fixed glassmorphism CSS variables and theme switching

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 14:56:21 -06:00

1698 lines
68 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import Layout from '../layouts/Layout.astro';
import PublicHeader from '../components/PublicHeader.astro';
import { verifyAuth } from '../lib/auth';
// Enable server-side rendering for auth checks
export const prerender = false;
// Required authentication check for calendar access
const auth = await verifyAuth(Astro.request);
if (!auth) {
return Astro.redirect('/login-new');
}
// Get query parameters for filtering
const url = new URL(Astro.request.url);
const featured = url.searchParams.get('featured');
const category = url.searchParams.get('category');
const search = url.searchParams.get('search');
// Add environment variable for Mapbox (if needed for geocoding)
// const mapboxToken = import.meta.env.PUBLIC_MAPBOX_TOKEN || ''; // Commented out - not needed in script
---
<Layout title="Event Calendar - Black Canyon Tickets">
<div class="min-h-screen" data-theme-background>
<!-- Hero Section with Dynamic Background -->
<section id="hero-section" class="relative overflow-hidden sticky top-0 z-40" style="background: var(--bg-gradient);">
<PublicHeader showCalendarNav={true} />
<!-- Theme Toggle -->
<div class="fixed top-20 right-4 z-50">
<button
id="theme-toggle"
class="p-3 rounded-full backdrop-blur-lg transition-all duration-200 hover:scale-110 shadow-lg"
style="background: var(--glass-bg-button); border: 1px solid var(--glass-border);"
aria-label="Toggle theme"
onclick="toggleTheme()"
>
<svg class="w-5 h-5" style="color: var(--glass-text-primary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
</button>
</div>
<!-- Animated Background Elements -->
<div class="absolute inset-0 opacity-20">
<div class="absolute top-20 left-20 w-64 h-64 rounded-full blur-3xl animate-pulse" style="background: var(--bg-orb-1);"></div>
<div class="absolute bottom-20 right-20 w-96 h-96 rounded-full blur-3xl animate-pulse delay-1000" style="background: var(--bg-orb-2);"></div>
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-80 h-80 rounded-full blur-3xl animate-pulse delay-500" style="background: var(--bg-orb-3);"></div>
</div>
<!-- Geometric Patterns -->
<div class="absolute inset-0" style="opacity: var(--grid-opacity, 0.1);">
<svg class="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice">
<defs>
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="currentColor" stroke-width="0.5" style="color: var(--grid-pattern);"/>
</pattern>
</defs>
<rect width="100" height="100" fill="url(#grid)" />
</svg>
</div>
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-32 pb-24 lg:pt-40 lg:pb-32">
<div class="text-center">
<!-- Badge -->
<div class="inline-flex items-center px-4 py-2 rounded-full backdrop-blur-lg mb-8" style="background: var(--glass-bg); border: 1px solid var(--glass-border);">
<span class="text-sm font-medium" style="color: var(--glass-text-secondary);">✨ Discover Extraordinary Events</span>
</div>
<!-- Main Heading -->
<h1 class="text-5xl lg:text-7xl font-light mb-6 tracking-tight" style="color: var(--glass-text-primary);">
Event
<span class="font-bold" style="color: var(--glass-text-accent);">
Calendar
</span>
</h1>
<!-- Subheading -->
<p class="text-xl lg:text-2xl mb-8 max-w-3xl mx-auto leading-relaxed" style="color: var(--glass-text-secondary);">
Curated experiences in Colorado's most prestigious venues. From intimate galas to grand celebrations.
</p>
<!-- Location Detection -->
<div class="max-w-md mx-auto mb-8">
<div id="location-detector" class="backdrop-blur-xl rounded-xl p-4 transition-all duration-300" style="background: var(--glass-bg-lg); border: 1px solid var(--glass-border);">
<div id="location-status" class="flex items-center justify-center space-x-2">
<svg class="w-5 h-5" style="color: var(--glass-text-secondary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<button id="enable-location" class="font-medium" style="color: var(--glass-text-secondary);">
Enable location for personalized events
</button>
</div>
</div>
</div>
<!-- Advanced Search Bar -->
<div class="max-w-2xl mx-auto">
<div class="relative group">
<div class="absolute inset-0 rounded-2xl blur opacity-20 group-hover:opacity-30 transition-opacity" style="background: linear-gradient(to right, rgb(37, 99, 235), rgb(147, 51, 234));"></div>
<div class="relative backdrop-blur-xl rounded-2xl p-2 flex flex-col sm:flex-row items-stretch sm:items-center gap-2 sm:gap-2" style="background: var(--glass-bg-lg); border: 1px solid var(--glass-border);">
<div class="flex-1 flex items-center space-x-3 px-3 sm:px-4">
<svg class="w-4 h-4 sm:w-5 sm:h-5 flex-shrink-0" style="color: var(--glass-text-tertiary);" 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>
<input
type="text"
id="search-input"
placeholder="Search events, venues, or organizers..."
class="bg-transparent focus:outline-none flex-1 text-base sm:text-lg py-2 sm:py-0"
style="color: var(--glass-text-primary);"
placeholder="Search events, venues, or organizers..."
value={search || ''}
/>
</div>
<button
id="search-btn"
class="bg-gradient-to-r px-6 sm:px-8 py-3 rounded-xl font-semibold transition-all duration-200 shadow-lg hover:shadow-xl touch-manipulation min-h-[44px] text-sm sm:text-base"
style="background: linear-gradient(to right, rgb(37, 99, 235), rgb(147, 51, 234)); color: var(--glass-text-primary);"
>
Search
</button>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- What's Hot Section -->
<section id="whats-hot-section" class="py-8 hidden" style="background: var(--ui-bg-secondary);">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center space-x-2">
<span class="text-2xl">🔥</span>
<h2 class="text-2xl font-bold" style="color: var(--ui-text-primary);">What's Hot Near You</h2>
</div>
<span id="hot-location-text" class="text-sm" style="color: var(--ui-text-secondary);"></span>
</div>
<div id="hot-events-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Hot events will be populated here -->
</div>
</div>
</section>
<!-- Premium Filter Controls -->
<section class="sticky top-0 z-50 backdrop-blur-xl shadow-2xl" data-filter-controls style="background: var(--glass-bg-lg); border-bottom: 1px solid var(--glass-border);">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div class="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4">
<!-- View Toggle - Premium Design -->
<div class="flex items-center space-x-4">
<span class="text-sm font-semibold tracking-wide" style="color: var(--glass-text-secondary);">VIEW</span>
<div class="flex rounded-xl p-1 shadow-lg" style="background: var(--glass-bg-button); border: 1px solid var(--glass-border);">
<button
id="calendar-view-btn"
class="px-6 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 flex items-center gap-2 hover:scale-105 shadow-sm"
style="background: var(--glass-bg-elevated); color: var(--glass-text-primary);"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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>
Calendar
</button>
<button
id="list-view-btn"
class="px-6 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 flex items-center gap-2 hover:scale-105"
style="color: var(--glass-text-secondary);"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"></path>
</svg>
List
</button>
</div>
</div>
<!-- Premium Filters -->
<div class="flex flex-wrap items-center gap-3">
<!-- Location Display -->
<div id="location-display" class="hidden items-center space-x-2 px-4 py-2 rounded-xl shadow-lg transition-all duration-200" style="background: var(--glass-bg-button); border: 1px solid var(--glass-border);">
<svg class="w-4 h-4" style="color: var(--glass-text-accent);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span id="location-text" class="text-sm font-medium" style="color: var(--glass-text-accent);"></span>
<button id="clear-location" class="text-sm ml-2 px-2 py-1 rounded-full transition-all duration-200 hover:scale-110" style="color: var(--glass-text-accent); background: var(--glass-bg);">×</button>
</div>
<!-- Distance Filter -->
<div id="distance-filter" class="relative hidden">
<select
id="radius-filter"
class="appearance-none rounded-xl px-4 py-2.5 pr-10 text-sm font-medium backdrop-blur-lg transition-all duration-200 shadow-lg hover:shadow-xl focus:ring-2"
style="background: var(--glass-bg-input); border: 1px solid var(--glass-border); color: var(--glass-text-primary); focus:ring-color: var(--glass-border-focus);"
>
<option value="10">Within 10 miles</option>
<option value="25" selected>Within 25 miles</option>
<option value="50">Within 50 miles</option>
<option value="100">Within 100 miles</option>
</select>
<div class="absolute inset-y-0 right-0 flex items-center px-3 pointer-events-none">
<svg class="w-4 h-4" style="color: var(--glass-text-tertiary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>
<!-- Category Filter -->
<div class="relative">
<select
id="category-filter"
class="appearance-none rounded-xl px-4 py-2.5 pr-10 text-sm font-medium backdrop-blur-lg transition-all duration-200 shadow-lg hover:shadow-xl focus:ring-2"
style="background: var(--glass-bg-input); border: 1px solid var(--glass-border); color: var(--glass-text-primary); focus:ring-color: var(--glass-border-focus);"
>
<option value="">All Categories</option>
<option value="music" {category === 'music' ? 'selected' : ''}>Music & Concerts</option>
<option value="arts" {category === 'arts' ? 'selected' : ''}>Arts & Culture</option>
<option value="community" {category === 'community' ? 'selected' : ''}>Community Events</option>
<option value="business" {category === 'business' ? 'selected' : ''}>Business & Networking</option>
<option value="food" {category === 'food' ? 'selected' : ''}>Food & Wine</option>
<option value="sports" {category === 'sports' ? 'selected' : ''}>Sports & Recreation</option>
</select>
<div class="absolute inset-y-0 right-0 flex items-center px-3 pointer-events-none">
<svg class="w-4 h-4" style="color: var(--glass-text-tertiary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>
<!-- Date Range Filter -->
<div class="relative">
<select
id="date-filter"
class="appearance-none rounded-xl px-4 py-2.5 pr-10 text-sm font-medium backdrop-blur-lg transition-all duration-200 shadow-lg hover:shadow-xl focus:ring-2"
style="background: var(--glass-bg-input); border: 1px solid var(--glass-border); color: var(--glass-text-primary); focus:ring-color: var(--glass-border-focus);"
>
<option value="">All Dates</option>
<option value="today">Today</option>
<option value="tomorrow">Tomorrow</option>
<option value="this-week">This Week</option>
<option value="this-weekend">This Weekend</option>
<option value="next-week">Next Week</option>
<option value="this-month">This Month</option>
<option value="next-month">Next Month</option>
</select>
<div class="absolute inset-y-0 right-0 flex items-center px-3 pointer-events-none">
<svg class="w-4 h-4" style="color: var(--glass-text-tertiary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>
<!-- Featured Toggle -->
<label class="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
id="featured-filter"
class="rounded transition-all duration-200"
style="border-color: var(--glass-border); color: var(--glass-text-accent); focus:ring-color: var(--glass-text-accent);"
{featured ? 'checked' : ''}
/>
<span class="text-sm font-medium" style="color: var(--glass-text-secondary);">Featured Only</span>
</label>
<!-- Clear Filters -->
<button
id="clear-filters"
class="text-sm font-medium transition-all duration-200 px-3 py-1.5 rounded-lg hover:scale-105"
style="color: var(--glass-text-tertiary); background: var(--glass-bg-button);"
>
Clear All
</button>
</div>
</div>
</div>
</section>
<!-- Main Content -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Loading State -->
<div id="loading-state" class="text-center py-16">
<div class="inline-flex items-center space-x-2">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span class="text-lg font-medium" style="color: var(--ui-text-secondary);">Loading events...</span>
</div>
</div>
<!-- Calendar View -->
<div id="calendar-view" class="hidden">
<!-- Calendar Header -->
<div class="flex items-center justify-between mb-4 md:mb-8">
<div class="flex items-center space-x-2 md:space-x-4">
<button
id="prev-month"
class="p-2 md:p-3 rounded-lg transition-all duration-200 touch-manipulation min-h-[44px] min-w-[44px] flex items-center justify-center hover:scale-110"
style="background: var(--glass-bg-button); color: var(--glass-text-secondary);"
>
<svg class="w-4 h-4 md:w-5 md:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
<h2 id="calendar-month" class="text-lg md:text-2xl font-bold" style="color: var(--ui-text-primary);"></h2>
<button
id="next-month"
class="p-2 md:p-3 rounded-lg transition-all duration-200 touch-manipulation min-h-[44px] min-w-[44px] flex items-center justify-center hover:scale-110"
style="background: var(--glass-bg-button); color: var(--glass-text-secondary);"
>
<svg class="w-4 h-4 md:w-5 md:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
<button
id="today-btn"
class="px-4 md:px-5 py-2 md:py-2.5 rounded-lg transition-all duration-200 font-medium text-sm md:text-base shadow-lg hover:shadow-xl touch-manipulation min-h-[44px] hover:scale-105"
style="background: linear-gradient(to right, rgb(37, 99, 235), rgb(147, 51, 234)); color: var(--glass-text-primary);"
>
Today
</button>
</div>
<!-- Calendar Grid -->
<div class="rounded-2xl shadow-xl overflow-hidden backdrop-blur-lg" style="background: var(--glass-bg-lg); border: 1px solid var(--glass-border);">
<!-- Day Headers - Responsive -->
<div class="grid grid-cols-7" style="background: var(--ui-bg-secondary); border-bottom: 1px solid var(--ui-border-secondary);">
<div class="p-2 md:p-4 text-center text-xs md:text-sm font-semibold" style="color: var(--ui-text-secondary);">
<span class="hidden md:inline">Sunday</span>
<span class="md:hidden">Sun</span>
</div>
<div class="p-2 md:p-4 text-center text-xs md:text-sm font-semibold" style="color: var(--ui-text-secondary);">
<span class="hidden md:inline">Monday</span>
<span class="md:hidden">Mon</span>
</div>
<div class="p-2 md:p-4 text-center text-xs md:text-sm font-semibold" style="color: var(--ui-text-secondary);">
<span class="hidden md:inline">Tuesday</span>
<span class="md:hidden">Tue</span>
</div>
<div class="p-2 md:p-4 text-center text-xs md:text-sm font-semibold" style="color: var(--ui-text-secondary);">
<span class="hidden md:inline">Wednesday</span>
<span class="md:hidden">Wed</span>
</div>
<div class="p-2 md:p-4 text-center text-xs md:text-sm font-semibold" style="color: var(--ui-text-secondary);">
<span class="hidden md:inline">Thursday</span>
<span class="md:hidden">Thu</span>
</div>
<div class="p-2 md:p-4 text-center text-xs md:text-sm font-semibold" style="color: var(--ui-text-secondary);">
<span class="hidden md:inline">Friday</span>
<span class="md:hidden">Fri</span>
</div>
<div class="p-2 md:p-4 text-center text-xs md:text-sm font-semibold" style="color: var(--ui-text-secondary);">
<span class="hidden md:inline">Saturday</span>
<span class="md:hidden">Sat</span>
</div>
</div>
<!-- Calendar Days -->
<div id="calendar-grid" class="grid grid-cols-7 gap-px" style="background: var(--ui-border-secondary);">
<!-- Days will be populated by JavaScript -->
</div>
</div>
</div>
<!-- List View -->
<div id="list-view" class="hidden">
<div id="events-list" class="space-y-6">
<!-- Events will be populated by JavaScript -->
</div>
</div>
<!-- Empty State -->
<div id="empty-state" class="hidden text-center py-16">
<div class="max-w-md mx-auto">
<div class="w-24 h-24 mx-auto mb-6 rounded-full flex items-center justify-center" style="background: linear-gradient(to bottom right, var(--ui-bg-secondary), var(--ui-bg-elevated));">
<svg class="w-12 h-12" style="color: var(--ui-text-muted);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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>
</div>
<h3 class="text-xl font-semibold mb-2" style="color: var(--ui-text-primary);">No Events Found</h3>
<p class="mb-6" style="color: var(--ui-text-secondary);">Try adjusting your filters or search terms to find events.</p>
<button
id="clear-filters-empty"
class="inline-flex items-center px-4 py-2 rounded-lg transition-all duration-200 font-medium hover:scale-105"
style="background: linear-gradient(to right, rgb(37, 99, 235), rgb(147, 51, 234)); color: var(--glass-text-primary);"
>
Clear All Filters
</button>
</div>
</div>
</main>
<!-- Event Detail Modal -->
<div id="event-modal" class="fixed inset-0 z-50 hidden">
<div class="absolute inset-0 backdrop-blur-sm" style="background: rgba(0, 0, 0, 0.6);" id="modal-backdrop"></div>
<div class="relative min-h-screen flex items-center justify-center p-4">
<div class="rounded-2xl shadow-2xl w-full max-w-sm sm:max-w-lg lg:max-w-2xl max-h-[90vh] overflow-y-auto backdrop-blur-xl" style="background: var(--glass-bg-lg); border: 1px solid var(--glass-border);">
<div id="modal-content" class="p-4 sm:p-6 lg:p-8">
<!-- Modal content will be populated by JavaScript -->
</div>
</div>
</div>
</div>
</div>
</Layout>
<style>
/* Import glassmorphism theme styles */
@import '../styles/glassmorphism.css';
/* Custom animations */
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes shimmer {
0% { background-position: -1000px 0; }
100% { background-position: 1000px 0; }
}
.animate-float {
animation: float 6s ease-in-out infinite;
}
.animate-fade-in-up {
animation: fadeInUp 0.6s ease-out;
}
.shimmer {
background: linear-gradient(
90deg,
transparent,
rgba(255,255,255,0.4),
transparent
);
background-size: 1000px 100%;
animation: shimmer 2s infinite;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--ui-bg-secondary);
}
::-webkit-scrollbar-thumb {
background: linear-gradient(45deg, rgb(37, 99, 235), rgb(147, 51, 234));
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(45deg, rgb(29, 78, 216), rgb(126, 34, 206));
}
/* Calendar day hover effects */
.calendar-day {
transition: all 0.3s ease;
background: var(--ui-bg-elevated);
}
.calendar-day:hover {
background: linear-gradient(135deg, var(--ui-bg-secondary), var(--ui-bg-elevated));
}
@media (min-width: 768px) {
.calendar-day:hover {
transform: scale(1.02);
}
}
/* Event card animations */
.event-card {
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.event-card:hover {
transform: translateY(-8px);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
/* Glassmorphism effects */
.glass {
background: var(--glass-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
}
</style>
<script>
console.log('=== CALENDAR SCRIPT STARTING ===');
// Theme management system
function getCurrentTheme() {
if (typeof window === 'undefined') return 'dark';
const savedTheme = localStorage.getItem('theme');
if (savedTheme) return savedTheme;
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function setTheme(theme) {
if (typeof window === 'undefined') return;
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add(theme);
document.body.classList.remove('light', 'dark');
document.body.classList.add(theme);
localStorage.setItem('theme', theme);
// Dispatch theme change event
window.dispatchEvent(new CustomEvent('themeChanged', {
detail: { theme }
}));
}
function initializeTheme() {
if (typeof window === 'undefined') return;
const savedTheme = getCurrentTheme();
setTheme(savedTheme);
}
// Theme toggle function
window.toggleTheme = function() {
const currentTheme = getCurrentTheme();
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
// Update icon
const toggle = document.getElementById('theme-toggle');
const icon = toggle?.querySelector('svg path');
if (icon) {
if (newTheme === 'light') {
icon.setAttribute('d', 'M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z');
} else {
icon.setAttribute('d', 'M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z');
}
}
console.log('Theme toggled to:', newTheme);
return newTheme;
};
// Initialize theme immediately
initializeTheme();
// Handle theme changes and update background
function updateBackground() {
const theme = getCurrentTheme();
const bgElement = document.querySelector('[data-theme-background]');
if (bgElement) {
if (theme === 'light') {
bgElement.style.background = '#f8fafc';
} else {
bgElement.style.background = 'var(--bg-gradient)';
}
}
}
// Listen for theme changes
window.addEventListener('themeChanged', updateBackground);
// Initial background update
updateBackground();
// Import geolocation utilities - get from environment or default to empty
const MAPBOX_TOKEN = '';
// Calendar state
let currentDate = new Date();
let currentView = 'calendar';
let events = [];
let filteredEvents = [];
let userLocation = null;
let currentRadius = 25;
// DOM elements
const loadingState = document.getElementById('loading-state');
const calendarView = document.getElementById('calendar-view');
const listView = document.getElementById('list-view');
const emptyState = document.getElementById('empty-state');
const calendarGrid = document.getElementById('calendar-grid');
const eventsList = document.getElementById('events-list');
const calendarMonth = document.getElementById('calendar-month');
const eventModal = document.getElementById('event-modal');
const modalContent = document.getElementById('modal-content');
const modalBackdrop = document.getElementById('modal-backdrop');
// Filter elements
const searchInput = document.getElementById('search-input');
const searchBtn = document.getElementById('search-btn');
const categoryFilter = document.getElementById('category-filter');
const dateFilter = document.getElementById('date-filter');
const featuredFilter = document.getElementById('featured-filter');
const clearFiltersBtn = document.getElementById('clear-filters');
const clearFiltersEmptyBtn = document.getElementById('clear-filters-empty');
// View toggle elements
const calendarViewBtn = document.getElementById('calendar-view-btn');
const listViewBtn = document.getElementById('list-view-btn');
// Calendar navigation
const prevMonthBtn = document.getElementById('prev-month');
const nextMonthBtn = document.getElementById('next-month');
const todayBtn = document.getElementById('today-btn');
// Location elements
const enableLocationBtn = document.getElementById('enable-location');
const locationStatus = document.getElementById('location-status');
const locationDisplay = document.getElementById('location-display');
const locationText = document.getElementById('location-text');
const clearLocationBtn = document.getElementById('clear-location');
const distanceFilter = document.getElementById('distance-filter');
const radiusFilter = document.getElementById('radius-filter');
const whatsHotSection = document.getElementById('whats-hot-section');
const hotEventsGrid = document.getElementById('hot-events-grid');
const hotLocationText = document.getElementById('hot-location-text');
// Utility functions
function formatDate(date) {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date);
}
function formatTime(date) {
return new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
}).format(date);
}
function formatDateTime(dateString) {
const date = new Date(dateString);
return {
date: formatDate(date),
time: formatTime(date),
dayOfWeek: date.toLocaleDateString('en-US', { weekday: 'long' })
};
}
function getCategoryColor(category) {
const colors = {
music: 'from-purple-500 to-pink-500',
arts: 'from-blue-500 to-cyan-500',
community: 'from-green-500 to-emerald-500',
business: 'from-gray-500 to-slate-500',
food: 'from-orange-500 to-red-500',
sports: 'from-indigo-500 to-purple-500',
default: 'from-gray-400 to-gray-500'
};
return colors[category] || colors.default;
}
function getCategoryIcon(category) {
const icons = {
music: '🎵',
arts: '🎨',
community: '🤝',
business: '💼',
food: '🍷',
sports: '⚽',
default: '📅'
};
return icons[category] || icons.default;
}
// Location functions
async function requestLocationPermission() {
try {
// First try GPS location
if (navigator.geolocation) {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
async (position) => {
userLocation = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
source: 'gps'
};
await updateLocationDisplay();
resolve(userLocation);
},
async (error) => {
console.warn('GPS location failed, trying IP geolocation');
// Fall back to IP geolocation
const ipLocation = await getLocationFromIP();
if (ipLocation) {
userLocation = ipLocation;
await updateLocationDisplay();
resolve(userLocation);
} else {
reject(error);
}
},
{ enableHighAccuracy: true, timeout: 10000 }
);
});
} else {
// Try IP geolocation if browser doesn't support GPS
const ipLocation = await getLocationFromIP();
if (ipLocation) {
userLocation = ipLocation;
await updateLocationDisplay();
return userLocation;
}
}
} catch (error) {
console.error('Error getting location:', error);
return null;
}
}
async function getLocationFromIP() {
try {
const response = await fetch('https://ipapi.co/json/');
const data = await response.json();
if (data.latitude && data.longitude) {
return {
latitude: data.latitude,
longitude: data.longitude,
city: data.city,
state: data.region,
country: data.country_code,
source: 'ip'
};
}
} catch (error) {
console.warn('Error getting IP location:', error);
}
return null;
}
async function updateLocationDisplay() {
if (userLocation) {
// Update location status in hero
locationStatus.innerHTML = `
<svg class="w-5 h-5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="text-green-400 font-medium">Location enabled</span>
${userLocation.city ? `<span class="text-sm ml-2" style="color: var(--glass-text-tertiary);">(${userLocation.city})</span>` : ''}
`;
// Show location in filter bar
locationDisplay.classList.remove('hidden');
locationDisplay.classList.add('flex');
locationText.textContent = userLocation.city && userLocation.state ?
`${userLocation.city}, ${userLocation.state}` :
'Location detected';
// Show distance filter
distanceFilter.classList.remove('hidden');
// Load hot events
await loadHotEvents();
}
}
async function loadHotEvents() {
if (!userLocation) return;
try {
const response = await fetch(`/api/events/trending?lat=${userLocation.latitude}&lng=${userLocation.longitude}&radius=${currentRadius}&limit=4`);
if (!response.ok) throw new Error('Failed to fetch trending events');
const data = await response.json();
if (data.success && data.data.length > 0) {
displayHotEvents(data.data);
whatsHotSection.classList.remove('hidden');
hotLocationText.textContent = `Within ${currentRadius} miles`;
}
} catch (error) {
console.error('Error loading hot events:', error);
}
}
function displayHotEvents(hotEvents) {
hotEventsGrid.innerHTML = hotEvents.map(event => {
const categoryColor = getCategoryColor(event.category);
const categoryIcon = getCategoryIcon(event.category);
return `
<div class="rounded-xl shadow-lg hover:shadow-2xl transition-all duration-300 cursor-pointer transform hover:-translate-y-1 backdrop-blur-lg" style="background: var(--ui-bg-elevated); border: 1px solid var(--ui-border-primary);" onclick="showEventModal(${JSON.stringify(event).replace(/"/g, '&quot;')})">
<div class="relative">
<div class="h-32 bg-gradient-to-br ${categoryColor} rounded-t-xl flex items-center justify-center">
<span class="text-4xl">${categoryIcon}</span>
</div>
${event.popularityScore > 50 ? `
<div class="absolute top-2 right-2 px-2 py-1 rounded-full text-xs font-bold" style="background: var(--error-color); color: var(--glass-text-primary);">
HOT 🔥
</div>
` : ''}
</div>
<div class="p-4">
<h3 class="font-bold mb-1 line-clamp-1" style="color: var(--ui-text-primary);">${event.title}</h3>
<p class="text-sm mb-2" style="color: var(--ui-text-secondary);">${event.venue}</p>
<div class="flex items-center justify-between text-xs" style="color: var(--ui-text-tertiary);">
<span>${event.distanceMiles ? `${event.distanceMiles.toFixed(1)} mi` : ''}</span>
<span>${event.ticketsSold || 0} sold</span>
</div>
</div>
</div>
`;
}).join('');
}
function clearLocation() {
userLocation = null;
locationStatus.innerHTML = `
<svg class="w-5 h-5" style="color: var(--glass-text-secondary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<button id="enable-location" class="font-medium hover:scale-105 transition-all duration-200" style="color: var(--glass-text-secondary);">
Enable location for personalized events
</button>
`;
locationDisplay.classList.add('hidden');
distanceFilter.classList.add('hidden');
whatsHotSection.classList.add('hidden');
// Re-attach event listener
document.getElementById('enable-location').addEventListener('click', enableLocation);
// Reload events without location filtering
loadEvents();
}
async function enableLocation() {
const btn = event.target;
btn.textContent = 'Getting location...';
btn.disabled = true;
try {
await requestLocationPermission();
if (userLocation) {
await loadEvents(); // Reload events with location data
}
} catch (error) {
console.error('Location error:', error);
btn.textContent = 'Location unavailable';
setTimeout(() => {
btn.textContent = 'Enable location for personalized events';
btn.disabled = false;
}, 3000);
}
}
// API functions
async function fetchEvents(params = {}) {
try {
const url = new URL('/api/public/events', window.location.origin);
// Add location parameters if available
if (userLocation && currentRadius) {
params.lat = userLocation.latitude;
params.lng = userLocation.longitude;
params.radius = currentRadius;
}
Object.entries(params).forEach(([key, value]) => {
if (value) url.searchParams.append(key, value);
});
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to fetch events');
const data = await response.json();
return data.events || [];
} catch (error) {
// Silently handle errors in production
return [];
}
}
// Filter functions
function getFilterParams() {
const params = {};
if (searchInput.value.trim()) {
params.search = searchInput.value.trim();
}
if (categoryFilter.value) {
params.category = categoryFilter.value;
}
if (featuredFilter.checked) {
params.featured = 'true';
}
// Handle date filter
const dateFilterValue = dateFilter.value;
if (dateFilterValue) {
const now = new Date();
let startDate, endDate;
switch (dateFilterValue) {
case 'today':
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + 1);
break;
case 'tomorrow':
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + 1);
break;
case 'this-week':
const dayOfWeek = now.getDay();
startDate = new Date(now);
startDate.setDate(now.getDate() - dayOfWeek);
endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + 7);
break;
case 'this-weekend':
const daysUntilSaturday = 6 - now.getDay();
startDate = new Date(now);
startDate.setDate(now.getDate() + daysUntilSaturday);
endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + 2);
break;
case 'next-week':
const daysUntilNextWeek = 7 - now.getDay();
startDate = new Date(now);
startDate.setDate(now.getDate() + daysUntilNextWeek);
endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + 7);
break;
case 'this-month':
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
endDate = new Date(now.getFullYear(), now.getMonth() + 1, 1);
break;
case 'next-month':
startDate = new Date(now.getFullYear(), now.getMonth() + 1, 1);
endDate = new Date(now.getFullYear(), now.getMonth() + 2, 1);
break;
}
if (startDate && endDate) {
params.start_date = startDate.toISOString().split('T')[0];
params.end_date = endDate.toISOString().split('T')[0];
}
}
return params;
}
function clearAllFilters() {
searchInput.value = '';
categoryFilter.value = '';
dateFilter.value = '';
featuredFilter.checked = false;
updateURL();
loadEvents();
}
function updateURL() {
const params = getFilterParams();
const url = new URL(window.location);
// Clear existing params
url.search = '';
// Add new params
Object.entries(params).forEach(([key, value]) => {
if (value) url.searchParams.set(key, value);
});
// Update URL without reloading
window.history.replaceState({}, '', url);
}
// Event loading and filtering
async function loadEvents() {
showLoading();
const params = getFilterParams();
events = await fetchEvents(params);
filteredEvents = events;
hideLoading();
renderCurrentView();
}
function showLoading() {
loadingState.classList.remove('hidden');
calendarView.classList.add('hidden');
listView.classList.add('hidden');
emptyState.classList.add('hidden');
}
function hideLoading() {
loadingState.classList.add('hidden');
}
function showEmptyState() {
emptyState.classList.remove('hidden');
calendarView.classList.add('hidden');
listView.classList.add('hidden');
}
function renderCurrentView() {
if (filteredEvents.length === 0) {
showEmptyState();
return;
}
emptyState.classList.add('hidden');
if (currentView === 'calendar') {
renderCalendarView();
} else {
renderListView();
}
}
// Calendar rendering
function renderCalendarView() {
calendarView.classList.remove('hidden');
listView.classList.add('hidden');
updateCalendarHeader();
renderCalendarGrid();
}
function updateCalendarHeader() {
const monthName = currentDate.toLocaleDateString('en-US', {
month: 'long',
year: 'numeric'
});
calendarMonth.textContent = monthName;
}
function renderCalendarGrid() {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
// Get first day of month and number of days
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const startingDayOfWeek = firstDay.getDay();
// Calculate previous month days to show
const prevMonth = new Date(year, month - 1, 0);
const daysInPrevMonth = prevMonth.getDate();
calendarGrid.innerHTML = '';
// Generate calendar days
const totalCells = Math.ceil((daysInMonth + startingDayOfWeek) / 7) * 7;
for (let i = 0; i < totalCells; i++) {
const dayElement = createCalendarDay(i, startingDayOfWeek, daysInMonth, daysInPrevMonth, year, month);
calendarGrid.appendChild(dayElement);
}
}
function createCalendarDay(index, startingDayOfWeek, daysInMonth, daysInPrevMonth, year, month) {
const dayDiv = document.createElement('div');
dayDiv.className = 'calendar-day min-h-[80px] md:min-h-[120px] p-1 md:p-3 transition-all duration-300 cursor-pointer group';
dayDiv.style.borderBottom = '1px solid var(--ui-border-secondary)';
let dayNumber, isCurrentMonth, currentDayDate;
if (index < startingDayOfWeek) {
// Previous month days
dayNumber = daysInPrevMonth - (startingDayOfWeek - index - 1);
isCurrentMonth = false;
currentDayDate = new Date(year, month - 1, dayNumber);
} else if (index >= startingDayOfWeek + daysInMonth) {
// Next month days
dayNumber = index - startingDayOfWeek - daysInMonth + 1;
isCurrentMonth = false;
currentDayDate = new Date(year, month + 1, dayNumber);
} else {
// Current month days
dayNumber = index - startingDayOfWeek + 1;
isCurrentMonth = true;
currentDayDate = new Date(year, month, dayNumber);
}
// Check if it's today
const today = new Date();
const isToday = currentDayDate.toDateString() === today.toDateString();
// Get events for this day
const dayEvents = filteredEvents.filter(event => {
const eventDate = new Date(event.start_time);
return eventDate.toDateString() === currentDayDate.toDateString();
});
// Create day number
const dayNumberSpan = document.createElement('span');
dayNumberSpan.className = `text-xs md:text-sm font-medium ${
isCurrentMonth
? isToday
? 'px-1 md:px-2 py-0.5 md:py-1 rounded-full'
: ''
: ''
}`;
if (isCurrentMonth) {
if (isToday) {
dayNumberSpan.style.background = 'linear-gradient(to right, rgb(37, 99, 235), rgb(147, 51, 234))';
dayNumberSpan.style.color = 'var(--glass-text-primary)';
} else {
dayNumberSpan.style.color = 'var(--ui-text-primary)';
}
} else {
dayNumberSpan.style.color = 'var(--ui-text-muted)';
}
dayNumberSpan.textContent = dayNumber;
dayDiv.appendChild(dayNumberSpan);
// Add events
if (dayEvents.length > 0 && isCurrentMonth) {
const eventsContainer = document.createElement('div');
eventsContainer.className = 'mt-1 md:mt-2 space-y-0.5 md:space-y-1';
// Show fewer events on mobile
const isMobile = window.innerWidth < 768;
const maxVisibleEvents = isMobile ? 1 : 3;
const visibleEvents = dayEvents.slice(0, maxVisibleEvents);
const remainingCount = dayEvents.length - visibleEvents.length;
visibleEvents.forEach(event => {
const eventDiv = document.createElement('div');
const categoryColor = getCategoryColor(event.category);
eventDiv.className = `text-xs px-1 md:px-2 py-0.5 md:py-1 rounded-md bg-gradient-to-r ${categoryColor} font-medium cursor-pointer transform hover:scale-105 transition-all duration-200 shadow-sm hover:shadow-md truncate`;
eventDiv.style.color = 'var(--glass-text-primary)';
const maxTitleLength = isMobile ? 10 : 20;
eventDiv.textContent = event.title.length > maxTitleLength ? event.title.substring(0, maxTitleLength) + '...' : event.title;
eventDiv.title = event.title; // Full title on hover
eventDiv.addEventListener('click', (e) => {
e.stopPropagation();
showEventModal(event);
});
eventsContainer.appendChild(eventDiv);
});
if (remainingCount > 0) {
const moreDiv = document.createElement('div');
moreDiv.className = 'text-xs font-medium px-1 md:px-2 py-0.5 md:py-1 rounded-md transition-colors cursor-pointer';
moreDiv.style.color = 'var(--ui-text-secondary)';
moreDiv.style.background = 'var(--ui-bg-secondary)';
moreDiv.addEventListener('mouseenter', () => {
moreDiv.style.background = 'var(--ui-bg-elevated)';
});
moreDiv.addEventListener('mouseleave', () => {
moreDiv.style.background = 'var(--ui-bg-secondary)';
});
moreDiv.textContent = `+${remainingCount}`;
moreDiv.addEventListener('click', (e) => {
e.stopPropagation();
// Could show a day view modal here
showDayViewModal(currentDayDate, dayEvents);
});
eventsContainer.appendChild(moreDiv);
}
dayDiv.appendChild(eventsContainer);
}
return dayDiv;
}
// List view rendering
function renderListView() {
listView.classList.remove('hidden');
calendarView.classList.add('hidden');
eventsList.innerHTML = '';
// Group events by date
const eventsByDate = {};
filteredEvents.forEach(event => {
const dateKey = new Date(event.start_time).toDateString();
if (!eventsByDate[dateKey]) {
eventsByDate[dateKey] = [];
}
eventsByDate[dateKey].push(event);
});
// Sort dates
const sortedDates = Object.keys(eventsByDate).sort((a, b) => new Date(a) - new Date(b));
sortedDates.forEach(dateKey => {
const dateGroup = createDateGroup(dateKey, eventsByDate[dateKey]);
eventsList.appendChild(dateGroup);
});
}
function createDateGroup(dateKey, events) {
const groupDiv = document.createElement('div');
groupDiv.className = 'animate-fade-in-up';
// Date header
const dateHeader = document.createElement('div');
dateHeader.className = 'mb-4';
const date = new Date(dateKey);
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
let dateText;
if (date.toDateString() === today.toDateString()) {
dateText = 'Today';
} else if (date.toDateString() === tomorrow.toDateString()) {
dateText = 'Tomorrow';
} else {
dateText = date.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
dateHeader.innerHTML = `
<h3 class="text-lg font-semibold mb-1" style="color: var(--ui-text-primary);">${dateText}</h3>
<div class="w-16 h-1 rounded-full" style="background: linear-gradient(to right, rgb(37, 99, 235), rgb(147, 51, 234));"></div>
`;
groupDiv.appendChild(dateHeader);
// Events for this date
const eventsContainer = document.createElement('div');
eventsContainer.className = 'grid gap-4 sm:grid-cols-2 lg:grid-cols-3';
events.forEach(event => {
const eventCard = createEventCard(event);
eventsContainer.appendChild(eventCard);
});
groupDiv.appendChild(eventsContainer);
return groupDiv;
}
function createEventCard(event) {
const card = document.createElement('div');
card.className = 'event-card rounded-xl shadow-lg overflow-hidden hover:shadow-2xl transform hover:-translate-y-2 transition-all duration-500 cursor-pointer group backdrop-blur-lg';
card.style.background = 'var(--ui-bg-elevated)';
card.style.border = '1px solid var(--ui-border-primary)';
const { date, time, dayOfWeek } = formatDateTime(event.start_time);
const categoryColor = getCategoryColor(event.category);
const categoryIcon = getCategoryIcon(event.category);
card.innerHTML = `
<div class="relative">
<div class="h-48 bg-gradient-to-br ${categoryColor} relative overflow-hidden">
<div class="absolute inset-0" style="background: rgba(0, 0, 0, 0.2);"></div>
<div class="absolute top-4 left-4">
<span class="text-3xl">${categoryIcon}</span>
</div>
<div class="absolute top-4 right-4">
<span class="backdrop-blur-sm px-2 py-1 rounded-full text-xs font-medium" style="background: rgba(255, 255, 255, 0.2); color: var(--glass-text-primary);">
${event.category?.charAt(0).toUpperCase() + event.category?.slice(1) || 'Event'}
</span>
</div>
<div class="absolute bottom-4 left-4 right-4">
<h3 class="text-xl font-bold mb-2 line-clamp-2 group-hover:text-yellow-200 transition-colors" style="color: var(--glass-text-primary);">
${event.title}
</h3>
</div>
<div class="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent"></div>
</div>
<div class="p-6">
<div class="flex items-center space-x-2 text-sm mb-3" style="color: var(--ui-text-secondary);">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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>${time}</span>
</div>
<div class="flex items-start space-x-2 text-sm mb-4" style="color: var(--ui-text-secondary);">
<svg class="w-4 h-4 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span class="line-clamp-2">${event.venue || 'Venue TBA'}</span>
</div>
${event.description ? `
<p class="text-sm mb-4 line-clamp-3" style="color: var(--ui-text-secondary);">${event.description}</p>
` : ''}
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
${event.featured ? `
<span class="bg-yellow-100 text-yellow-800 px-2 py-1 rounded-full text-xs font-medium">
⭐ Featured
</span>
` : ''}
</div>
<button class="bg-gradient-to-r ${categoryColor} px-4 py-2 rounded-lg font-medium text-sm hover:shadow-lg transform hover:scale-105 transition-all duration-200" style="color: var(--glass-text-primary);">
View Details
</button>
</div>
</div>
</div>
`;
card.addEventListener('click', () => showEventModal(event));
return card;
}
// Modal functions
function showEventModal(event) {
const { date, time, dayOfWeek } = formatDateTime(event.start_time);
const categoryColor = getCategoryColor(event.category);
const categoryIcon = getCategoryIcon(event.category);
modalContent.innerHTML = `
<div class="relative">
<div class="flex items-start justify-between mb-6">
<div class="flex items-center space-x-3">
<span class="text-3xl">${categoryIcon}</span>
<div>
<h2 class="text-2xl font-bold mb-1" style="color: var(--ui-text-primary);">${event.title}</h2>
<span class="bg-gradient-to-r ${categoryColor} px-3 py-1 rounded-full text-sm font-medium" style="color: var(--glass-text-primary);">
${event.category?.charAt(0).toUpperCase() + event.category?.slice(1) || 'Event'}
</span>
</div>
</div>
<button id="close-modal" class="transition-colors" style="color: var(--ui-text-muted);" onmouseover="this.style.color='var(--ui-text-secondary)'" onmouseout="this.style.color='var(--ui-text-muted)'">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4">
<div class="flex items-start space-x-3">
<div class="flex-shrink-0">
<svg class="w-5 h-5 mt-1" style="color: var(--ui-text-muted);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
</div>
<div>
<h4 class="font-semibold" style="color: var(--ui-text-primary);">Date & Time</h4>
<p style="color: var(--ui-text-secondary);">${dayOfWeek}, ${date}</p>
<p style="color: var(--ui-text-secondary);">${time}</p>
</div>
</div>
<div class="flex items-start space-x-3">
<div class="flex-shrink-0">
<svg class="w-5 h-5 mt-1" style="color: var(--ui-text-muted);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
</div>
<div>
<h4 class="font-semibold" style="color: var(--ui-text-primary);">Venue</h4>
<p style="color: var(--ui-text-secondary);">${event.venue || 'Venue information coming soon'}</p>
</div>
</div>
</div>
<div class="space-y-4">
${event.featured ? `
<div class="rounded-lg p-4 backdrop-blur-lg" style="background: linear-gradient(to right, var(--warning-bg), var(--premium-gold-bg)); border: 1px solid var(--warning-border);">
<div class="flex items-center space-x-2">
<span class="text-xl">⭐</span>
<span class="font-semibold text-yellow-800">Featured Event</span>
</div>
<p class="text-yellow-700 text-sm mt-1">This is a specially curated featured event.</p>
</div>
` : ''}
<div class="rounded-lg p-4 backdrop-blur-lg" style="background: linear-gradient(to right, var(--glass-bg), var(--glass-bg-lg)); border: 1px solid var(--glass-border);">
<h4 class="font-semibold mb-2" style="color: var(--ui-text-primary);">Event Details</h4>
<p class="text-sm" style="color: var(--ui-text-secondary);">Click the button below to view full event details and purchase tickets.</p>
</div>
</div>
</div>
${event.description ? `
<div>
<h4 class="font-semibold mb-2" style="color: var(--ui-text-primary);">Description</h4>
<p class="leading-relaxed" style="color: var(--ui-text-secondary);">${event.description}</p>
</div>
` : ''}
<div class="flex space-x-4 pt-6" style="border-top: 1px solid var(--ui-border-secondary);">
<a
href="/e/${event.slug || event.id}"
class="flex-1 bg-gradient-to-r ${categoryColor} py-3 px-6 rounded-lg font-semibold text-center hover:shadow-lg transform hover:scale-105 transition-all duration-200"
style="color: var(--glass-text-primary);"
>
View Event & Get Tickets
</a>
<button
id="share-event"
class="px-6 py-3 rounded-lg font-semibold transition-colors flex items-center space-x-2"
style="border: 1px solid var(--ui-border-primary); color: var(--ui-text-secondary);"
onmouseover="this.style.background='var(--ui-bg-secondary)'"
onmouseout="this.style.background='transparent'"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z"></path>
</svg>
<span>Share</span>
</button>
</div>
</div>
</div>
`;
eventModal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
// Add event listeners for modal
document.getElementById('close-modal').addEventListener('click', hideEventModal);
document.getElementById('share-event').addEventListener('click', () => shareEvent(event));
}
function hideEventModal() {
eventModal.classList.add('hidden');
document.body.style.overflow = 'auto';
}
function shareEvent(event) {
const eventUrl = `${window.location.origin}/e/${event.slug || event.id}`;
if (navigator.share) {
navigator.share({
title: event.title,
text: event.description || `Check out this event: ${event.title}`,
url: eventUrl
});
} else {
// Fallback: copy to clipboard
navigator.clipboard.writeText(eventUrl).then(() => {
// Show success feedback
showToast('Event link copied to clipboard!');
}).catch(() => {
// Fallback for older browsers
showToast('Please copy the link manually');
});
}
}
function showDayViewModal(date, events) {
// Simple implementation - could be enhanced
const eventTitles = events.map(e => e.title).join('\n• ');
const dateStr = date.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
showToast(`Events on ${dateStr}:\n• ${eventTitles}`);
}
function showToast(message) {
// Simple toast notification
const toast = document.createElement('div');
toast.className = 'fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform translate-x-full transition-transform duration-300 backdrop-blur-lg';
toast.style.background = 'var(--ui-bg-elevated)';
toast.style.color = 'var(--ui-text-primary)';
toast.style.border = '1px solid var(--ui-border-primary)';
toast.textContent = message;
document.body.appendChild(toast);
// Animate in
setTimeout(() => {
toast.classList.remove('translate-x-full');
}, 100);
// Animate out and remove
setTimeout(() => {
toast.classList.add('translate-x-full');
setTimeout(() => {
document.body.removeChild(toast);
}, 300);
}, 3000);
}
// View toggle functions
function switchToCalendarView() {
currentView = 'calendar';
calendarViewBtn.className = 'px-6 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 flex items-center gap-2 hover:scale-105 shadow-sm';
calendarViewBtn.style.background = 'var(--glass-bg-elevated)';
calendarViewBtn.style.color = 'var(--glass-text-primary)';
listViewBtn.className = 'px-6 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 flex items-center gap-2 hover:scale-105';
listViewBtn.style.background = 'transparent';
listViewBtn.style.color = 'var(--glass-text-secondary)';
renderCurrentView();
}
function switchToListView() {
currentView = 'list';
listViewBtn.className = 'px-6 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 flex items-center gap-2 hover:scale-105 shadow-sm';
listViewBtn.style.background = 'var(--glass-bg-elevated)';
listViewBtn.style.color = 'var(--glass-text-primary)';
calendarViewBtn.className = 'px-6 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 flex items-center gap-2 hover:scale-105';
calendarViewBtn.style.background = 'transparent';
calendarViewBtn.style.color = 'var(--glass-text-secondary)';
renderCurrentView();
}
// Calendar navigation functions
function goToPreviousMonth() {
currentDate.setMonth(currentDate.getMonth() - 1);
renderCalendarView();
}
function goToNextMonth() {
currentDate.setMonth(currentDate.getMonth() + 1);
renderCalendarView();
}
function goToToday() {
currentDate = new Date();
renderCalendarView();
}
// Event listeners
searchBtn.addEventListener('click', () => {
updateURL();
loadEvents();
});
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
updateURL();
loadEvents();
}
});
categoryFilter.addEventListener('change', () => {
updateURL();
loadEvents();
});
dateFilter.addEventListener('change', () => {
updateURL();
loadEvents();
});
featuredFilter.addEventListener('change', () => {
updateURL();
loadEvents();
});
clearFiltersBtn.addEventListener('click', clearAllFilters);
clearFiltersEmptyBtn.addEventListener('click', clearAllFilters);
calendarViewBtn.addEventListener('click', switchToCalendarView);
listViewBtn.addEventListener('click', switchToListView);
prevMonthBtn.addEventListener('click', goToPreviousMonth);
nextMonthBtn.addEventListener('click', goToNextMonth);
todayBtn.addEventListener('click', goToToday);
modalBackdrop.addEventListener('click', hideEventModal);
// Location event listeners
enableLocationBtn.addEventListener('click', enableLocation);
clearLocationBtn.addEventListener('click', clearLocation);
radiusFilter.addEventListener('change', async () => {
currentRadius = parseInt(radiusFilter.value);
await loadEvents();
await loadHotEvents();
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !eventModal.classList.contains('hidden')) {
hideEventModal();
}
});
// Handle window resize for mobile responsiveness
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
if (currentView === 'calendar') {
renderCalendarGrid();
}
}, 250);
});
// Initialize
loadEvents();
// Old theme toggle code removed - using simpler onclick approach
// Smooth sticky header behavior
window.initStickyHeader = function initStickyHeader() {
const heroSection = document.getElementById('hero-section');
const filterControls = document.querySelector('[data-filter-controls]');
if (!heroSection || !filterControls) {
// If elements not found, try again in 100ms
setTimeout(initStickyHeader, 100);
return;
}
// Add smooth transition styles
heroSection.style.transition = 'transform 0.3s ease-out, opacity 0.3s ease-out';
let lastScrollY = window.scrollY;
let isTransitioning = false;
function handleScroll() {
const currentScrollY = window.scrollY;
const heroHeight = heroSection.offsetHeight;
const filterControlsOffsetTop = filterControls.offsetTop;
// Calculate transition point - when filter controls should take over
const transitionThreshold = filterControlsOffsetTop - heroHeight;
if (currentScrollY >= transitionThreshold) {
// Smoothly transition hero out and let filter controls take over
if (!isTransitioning) {
isTransitioning = true;
heroSection.style.transform = 'translateY(-100%)';
heroSection.style.opacity = '0.8';
heroSection.style.zIndex = '20'; // Below filter controls (z-50)
// After transition, change position to avoid layout issues
setTimeout(() => {
heroSection.style.position = 'relative';
heroSection.style.top = 'auto';
}, 300);
}
} else {
// Hero section is visible and sticky
if (isTransitioning) {
isTransitioning = false;
heroSection.style.position = 'sticky';
heroSection.style.top = '0px';
heroSection.style.transform = 'translateY(0)';
heroSection.style.opacity = '1';
heroSection.style.zIndex = '40'; // Above content but below filter controls
}
}
lastScrollY = currentScrollY;
}
// Add scroll listener with throttling for performance
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
handleScroll();
ticking = false;
});
ticking = true;
}
}, { passive: true });
// Initial call
handleScroll();
}
// Initialize sticky header
initStickyHeader();
</script>