feat: Modularize event management system - 98.7% reduction in main file size
BREAKING CHANGES: - Refactored monolithic manage.astro (7,623 lines) into modular architecture - Original file backed up as manage-old.astro NEW ARCHITECTURE: ✅ 5 Utility Libraries: - event-management.ts: Event data operations & formatting - ticket-management.ts: Ticket CRUD operations & sales data - seating-management.ts: Seating map management & layout generation - sales-analytics.ts: Sales metrics, reporting & data export - marketing-kit.ts: Marketing asset generation & social media ✅ 5 Shared Components: - TicketTypeModal.tsx: Reusable ticket type creation/editing - SeatingMapModal.tsx: Advanced seating map editor with drag-and-drop - EmbedCodeModal.tsx: Widget embedding with customization - OrdersTable.tsx: Comprehensive orders table with sorting/pagination - AttendeesTable.tsx: Attendee management with export capabilities ✅ 11 Tab Components: - TicketsTab.tsx: Ticket management with card/list views - VenueTab.tsx: Seating map management & venue configuration - OrdersTab.tsx: Sales data & order management - AttendeesTab.tsx: Attendee check-in & management - PresaleTab.tsx: Presale code generation & tracking - DiscountTab.tsx: Discount code management - AddonsTab.tsx: Add-on product management - PrintedTab.tsx: Printed ticket barcode management - SettingsTab.tsx: Event configuration & custom fields - MarketingTab.tsx: Marketing kit with social media templates - PromotionsTab.tsx: Campaign & promotion management ✅ 4 Infrastructure Components: - TabNavigation.tsx: Responsive tab navigation system - EventManagement.tsx: Main orchestration component - EventHeader.astro: Event information header - QuickStats.astro: Statistics dashboard BENEFITS: - 98.7% reduction in main file size (7,623 → ~100 lines) - Dramatic improvement in maintainability and team collaboration - Component-level testing now possible - Reusable components across multiple features - Lazy loading support for better performance - Full TypeScript support with proper interfaces - Separation of concerns: business logic separated from UI 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,9 @@ 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 || '';
|
||||
---
|
||||
|
||||
<Layout title="Event Calendar - Black Canyon Tickets">
|
||||
@@ -49,10 +52,25 @@ const search = url.searchParams.get('search');
|
||||
</h1>
|
||||
|
||||
<!-- Subheading -->
|
||||
<p class="text-xl lg:text-2xl text-white/80 mb-12 max-w-3xl mx-auto leading-relaxed">
|
||||
<p class="text-xl lg:text-2xl text-white/80 mb-8 max-w-3xl mx-auto leading-relaxed">
|
||||
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="bg-white/10 backdrop-blur-xl border border-white/20 rounded-xl p-4 transition-all duration-300">
|
||||
<div id="location-status" class="flex items-center justify-center space-x-2">
|
||||
<svg class="w-5 h-5 text-white/80" 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="text-white/90 hover:text-white font-medium">
|
||||
Enable location for personalized events
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Search Bar -->
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="relative group">
|
||||
@@ -83,10 +101,26 @@ const search = url.searchParams.get('search');
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- What's Hot Section -->
|
||||
<section id="whats-hot-section" class="py-8 bg-gradient-to-br from-gray-50 to-gray-100 hidden">
|
||||
<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 text-gray-900">What's Hot Near You</h2>
|
||||
</div>
|
||||
<span id="hot-location-text" class="text-sm text-gray-600"></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>
|
||||
|
||||
<!-- Filter Controls -->
|
||||
<section class="sticky top-0 z-50 bg-white/95 backdrop-blur-xl border-b border-gray-200/50 shadow-lg">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3 md:py-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 md:gap-4">
|
||||
<!-- View Toggle -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm font-medium text-gray-700">View:</span>
|
||||
@@ -114,6 +148,34 @@ const search = url.searchParams.get('search');
|
||||
|
||||
<!-- Advanced Filters -->
|
||||
<div class="flex flex-wrap items-center space-x-4">
|
||||
<!-- Location Display -->
|
||||
<div id="location-display" class="hidden items-center space-x-2 bg-blue-50 px-3 py-1.5 rounded-lg">
|
||||
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
<span id="location-text" class="text-sm text-blue-800 font-medium"></span>
|
||||
<button id="clear-location" class="text-blue-600 hover:text-blue-800 text-xs">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Distance Filter -->
|
||||
<div id="distance-filter" class="relative hidden">
|
||||
<select
|
||||
id="radius-filter"
|
||||
class="appearance-none bg-white border border-gray-300 rounded-lg px-4 py-2 pr-8 text-sm font-medium text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<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-2 pointer-events-none">
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Filter -->
|
||||
<div class="relative">
|
||||
<select
|
||||
@@ -193,29 +255,29 @@ const search = url.searchParams.get('search');
|
||||
<!-- Calendar View -->
|
||||
<div id="calendar-view" class="hidden">
|
||||
<!-- Calendar Header -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center space-x-4">
|
||||
<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 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
class="p-1 md:p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4 md:w-5 md:h-5 text-gray-600" 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-2xl font-bold text-gray-900"></h2>
|
||||
<h2 id="calendar-month" class="text-lg md:text-2xl font-bold text-gray-900"></h2>
|
||||
<button
|
||||
id="next-month"
|
||||
class="p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
class="p-1 md:p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4 md:w-5 md:h-5 text-gray-600" 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 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
|
||||
class="px-3 md:px-4 py-1.5 md:py-2 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white rounded-lg transition-all duration-200 font-medium text-sm md:text-base shadow-lg hover:shadow-xl"
|
||||
>
|
||||
Today
|
||||
</button>
|
||||
@@ -223,19 +285,40 @@ const search = url.searchParams.get('search');
|
||||
|
||||
<!-- Calendar Grid -->
|
||||
<div class="bg-white rounded-2xl shadow-xl border border-gray-200/50 overflow-hidden">
|
||||
<!-- Day Headers -->
|
||||
<!-- Day Headers - Responsive -->
|
||||
<div class="grid grid-cols-7 bg-gradient-to-r from-gray-50 to-gray-100 border-b border-gray-200">
|
||||
<div class="p-4 text-center text-sm font-semibold text-gray-700">Sunday</div>
|
||||
<div class="p-4 text-center text-sm font-semibold text-gray-700">Monday</div>
|
||||
<div class="p-4 text-center text-sm font-semibold text-gray-700">Tuesday</div>
|
||||
<div class="p-4 text-center text-sm font-semibold text-gray-700">Wednesday</div>
|
||||
<div class="p-4 text-center text-sm font-semibold text-gray-700">Thursday</div>
|
||||
<div class="p-4 text-center text-sm font-semibold text-gray-700">Friday</div>
|
||||
<div class="p-4 text-center text-sm font-semibold text-gray-700">Saturday</div>
|
||||
<div class="p-2 md:p-4 text-center text-xs md:text-sm font-semibold text-gray-700">
|
||||
<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 text-gray-700">
|
||||
<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 text-gray-700">
|
||||
<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 text-gray-700">
|
||||
<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 text-gray-700">
|
||||
<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 text-gray-700">
|
||||
<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 text-gray-700">
|
||||
<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 divide-x divide-gray-200">
|
||||
<div id="calendar-grid" class="grid grid-cols-7 gap-px bg-gray-200">
|
||||
<!-- Days will be populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -345,11 +428,17 @@ const search = url.searchParams.get('search');
|
||||
/* Calendar day hover effects */
|
||||
.calendar-day {
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.calendar-day:hover {
|
||||
background: linear-gradient(135deg, #f8fafc, #e2e8f0);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.calendar-day:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
/* Event card animations */
|
||||
@@ -371,11 +460,16 @@ const search = url.searchParams.get('search');
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Import geolocation utilities
|
||||
const MAPBOX_TOKEN = '<%= mapboxToken %>';
|
||||
|
||||
// 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');
|
||||
@@ -407,6 +501,18 @@ const search = url.searchParams.get('search');
|
||||
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', {
|
||||
@@ -459,10 +565,200 @@ const search = url.searchParams.get('search');
|
||||
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-white/60 text-sm ml-2">(${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="bg-white rounded-xl shadow-lg hover:shadow-2xl transition-all duration-300 cursor-pointer transform hover:-translate-y-1" 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 bg-red-500 text-white px-2 py-1 rounded-full text-xs font-bold">
|
||||
HOT 🔥
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<h3 class="font-bold text-gray-900 mb-1 line-clamp-1">${event.title}</h3>
|
||||
<p class="text-sm text-gray-600 mb-2">${event.venue}</p>
|
||||
<div class="flex items-center justify-between text-xs text-gray-500">
|
||||
<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 text-white/80" 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="text-white/90 hover:text-white font-medium">
|
||||
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);
|
||||
});
|
||||
@@ -664,7 +960,7 @@ const search = url.searchParams.get('search');
|
||||
|
||||
function createCalendarDay(index, startingDayOfWeek, daysInMonth, daysInPrevMonth, year, month) {
|
||||
const dayDiv = document.createElement('div');
|
||||
dayDiv.className = 'calendar-day min-h-[120px] p-3 border-b border-gray-200 hover:bg-gradient-to-br hover:from-blue-50 hover:to-purple-50 transition-all duration-300 cursor-pointer group';
|
||||
dayDiv.className = 'calendar-day min-h-[80px] md:min-h-[120px] p-1 md:p-3 border-b border-gray-200 hover:bg-gradient-to-br hover:from-blue-50 hover:to-purple-50 transition-all duration-300 cursor-pointer group';
|
||||
|
||||
let dayNumber, isCurrentMonth, currentDayDate;
|
||||
|
||||
@@ -697,10 +993,10 @@ const search = url.searchParams.get('search');
|
||||
|
||||
// Create day number
|
||||
const dayNumberSpan = document.createElement('span');
|
||||
dayNumberSpan.className = `text-sm font-medium ${
|
||||
dayNumberSpan.className = `text-xs md:text-sm font-medium ${
|
||||
isCurrentMonth
|
||||
? isToday
|
||||
? 'bg-gradient-to-r from-blue-600 to-purple-600 text-white px-2 py-1 rounded-full'
|
||||
? 'bg-gradient-to-r from-blue-600 to-purple-600 text-white px-1 md:px-2 py-0.5 md:py-1 rounded-full'
|
||||
: 'text-gray-900'
|
||||
: 'text-gray-400'
|
||||
}`;
|
||||
@@ -711,17 +1007,21 @@ const search = url.searchParams.get('search');
|
||||
// Add events
|
||||
if (dayEvents.length > 0 && isCurrentMonth) {
|
||||
const eventsContainer = document.createElement('div');
|
||||
eventsContainer.className = 'mt-2 space-y-1';
|
||||
eventsContainer.className = 'mt-1 md:mt-2 space-y-0.5 md:space-y-1';
|
||||
|
||||
// Show up to 3 events, then a "more" indicator
|
||||
const visibleEvents = dayEvents.slice(0, 3);
|
||||
// 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-2 py-1 rounded-md bg-gradient-to-r ${categoryColor} text-white font-medium cursor-pointer transform hover:scale-105 transition-all duration-200 shadow-sm hover:shadow-md`;
|
||||
eventDiv.textContent = event.title.length > 20 ? event.title.substring(0, 20) + '...' : event.title;
|
||||
eventDiv.className = `text-xs px-1 md:px-2 py-0.5 md:py-1 rounded-md bg-gradient-to-r ${categoryColor} text-white font-medium cursor-pointer transform hover:scale-105 transition-all duration-200 shadow-sm hover:shadow-md truncate`;
|
||||
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);
|
||||
@@ -731,8 +1031,8 @@ const search = url.searchParams.get('search');
|
||||
|
||||
if (remainingCount > 0) {
|
||||
const moreDiv = document.createElement('div');
|
||||
moreDiv.className = 'text-xs text-gray-600 font-medium px-2 py-1 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors cursor-pointer';
|
||||
moreDiv.textContent = `+${remainingCount} more`;
|
||||
moreDiv.className = 'text-xs text-gray-600 font-medium px-1 md:px-2 py-0.5 md:py-1 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors cursor-pointer';
|
||||
moreDiv.textContent = `+${remainingCount}`;
|
||||
moreDiv.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
// Could show a day view modal here
|
||||
@@ -1130,6 +1430,15 @@ const search = url.searchParams.get('search');
|
||||
|
||||
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')) {
|
||||
@@ -1137,6 +1446,17 @@ const search = url.searchParams.get('search');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle window resize for mobile responsiveness
|
||||
let resizeTimer;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
if (currentView === 'calendar') {
|
||||
renderCalendarGrid();
|
||||
}
|
||||
}, 250);
|
||||
});
|
||||
|
||||
// Initialize
|
||||
loadEvents();
|
||||
</script>
|
||||
Reference in New Issue
Block a user