- 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>
1698 lines
68 KiB
Plaintext
1698 lines
68 KiB
Plaintext
---
|
||
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, '"')})">
|
||
<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> |