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:
2025-07-08 18:30:26 -06:00
parent 23f190c7a7
commit e8b95231b7
76 changed files with 26728 additions and 7101 deletions

View File

@@ -0,0 +1,285 @@
import React, { useState, useEffect } from 'react';
import { TrendingEvent, trendingAnalyticsService } from '../lib/analytics';
import { geolocationService, LocationData } from '../lib/geolocation';
interface WhatsHotEventsProps {
userLocation?: LocationData | null;
radius?: number;
limit?: number;
onEventClick?: (event: TrendingEvent) => void;
className?: string;
}
const WhatsHotEvents: React.FC<WhatsHotEventsProps> = ({
userLocation,
radius = 50,
limit = 8,
onEventClick,
className = ''
}) => {
const [trendingEvents, setTrendingEvents] = useState<TrendingEvent[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadTrendingEvents();
}, [userLocation, radius, limit]);
const loadTrendingEvents = async () => {
setIsLoading(true);
setError(null);
try {
let lat = userLocation?.latitude;
let lng = userLocation?.longitude;
// If no user location provided, try to get IP location
if (!lat || !lng) {
const ipLocation = await geolocationService.getLocationFromIP();
if (ipLocation) {
lat = ipLocation.latitude;
lng = ipLocation.longitude;
}
}
const trending = await trendingAnalyticsService.getTrendingEvents(
lat,
lng,
radius,
limit
);
setTrendingEvents(trending);
} catch (err) {
setError('Failed to load trending events');
console.error('Error loading trending events:', err);
} finally {
setIsLoading(false);
}
};
const handleEventClick = (event: TrendingEvent) => {
// Track the click event
trendingAnalyticsService.trackEvent({
eventId: event.eventId,
metricType: 'page_view',
sessionId: sessionStorage.getItem('sessionId') || 'anonymous',
locationData: userLocation ? {
latitude: userLocation.latitude,
longitude: userLocation.longitude,
city: userLocation.city,
state: userLocation.state
} : undefined
});
if (onEventClick) {
onEventClick(event);
} else {
// Navigate to event page
window.location.href = `/e/${event.slug}`;
}
};
const formatEventTime = (startTime: string) => {
const date = new Date(startTime);
const now = new Date();
const diffTime = date.getTime() - now.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
return 'Today';
} else if (diffDays === 1) {
return 'Tomorrow';
} else if (diffDays <= 7) {
return `${diffDays} days`;
} else {
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
});
}
};
const getPopularityBadge = (score: number) => {
if (score >= 100) return { text: 'Super Hot', color: 'bg-red-500' };
if (score >= 50) return { text: 'Hot', color: 'bg-orange-500' };
if (score >= 25) return { text: 'Trending', color: 'bg-yellow-500' };
return { text: 'Popular', color: 'bg-blue-500' };
};
if (isLoading) {
return (
<div className={`bg-white rounded-lg shadow-md p-6 ${className}`}>
<div className="flex items-center justify-center h-32">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div>
<span className="ml-3 text-gray-600">Loading hot events...</span>
</div>
</div>
);
}
if (error) {
return (
<div className={`bg-white rounded-lg shadow-md p-6 ${className}`}>
<div className="text-center">
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">Unable to load events</h3>
<p className="mt-1 text-sm text-gray-500">{error}</p>
<button
onClick={loadTrendingEvents}
className="mt-2 text-sm text-indigo-600 hover:text-indigo-700 font-medium"
>
Try again
</button>
</div>
</div>
);
}
if (trendingEvents.length === 0) {
return (
<div className={`bg-white rounded-lg shadow-md p-6 ${className}`}>
<div className="text-center">
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3a4 4 0 118 0v4m-4 12v-6m-4 0h8m-8 0v6a4 4 0 108 0v-6" />
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">No trending events found</h3>
<p className="mt-1 text-sm text-gray-500">
Try expanding your search radius or check back later
</p>
</div>
</div>
);
}
return (
<div className={`bg-white rounded-lg shadow-md overflow-hidden ${className}`}>
{/* Header */}
<div className="bg-gradient-to-r from-orange-400 to-red-500 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className="text-2xl">🔥</div>
<h2 className="text-xl font-bold text-white">What's Hot</h2>
</div>
{userLocation && (
<span className="text-orange-100 text-sm">
Within {radius} miles
</span>
)}
</div>
</div>
{/* Events Grid */}
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{trendingEvents.map((event, index) => {
const popularityBadge = getPopularityBadge(event.popularityScore);
return (
<div
key={event.eventId}
onClick={() => handleEventClick(event)}
className="group cursor-pointer bg-gray-50 rounded-lg p-4 hover:bg-gray-100 transition-colors duration-200 border border-gray-200 hover:border-gray-300 relative overflow-hidden"
>
{/* Popularity Badge */}
<div className={`absolute top-2 right-2 px-2 py-1 rounded-full text-xs font-medium text-white ${popularityBadge.color}`}>
{popularityBadge.text}
</div>
{/* Event Image */}
{event.imageUrl && (
<div className="w-full h-32 bg-gray-200 rounded-lg mb-3 overflow-hidden">
<img
src={event.imageUrl}
alt={event.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
/>
</div>
)}
{/* Event Content */}
<div className="space-y-2">
<div className="flex items-start justify-between">
<h3 className="text-sm font-semibold text-gray-900 line-clamp-2 pr-8">
{event.title}
</h3>
</div>
<div className="text-xs text-gray-600 space-y-1">
<div className="flex items-center">
<svg className="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={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 strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span className="truncate">{event.venue}</span>
</div>
{event.distanceMiles && (
<div className="flex items-center">
<svg className="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{event.distanceMiles.toFixed(1)} miles away</span>
</div>
)}
<div className="flex items-center">
<svg className="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{formatEventTime(event.startTime)}</span>
</div>
</div>
{/* Event Stats */}
<div className="flex items-center justify-between text-xs text-gray-500 pt-2 border-t border-gray-200">
<div className="flex items-center space-x-3">
<div className="flex items-center">
<svg className="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
<span>{event.viewCount || 0}</span>
</div>
<div className="flex items-center">
<svg className="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
</svg>
<span>{event.ticketsSold}</span>
</div>
</div>
{event.isFeature && (
<div className="flex items-center">
<svg className="h-3 w-3 mr-1 text-yellow-500" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
<span className="text-yellow-600 font-medium">Featured</span>
</div>
)}
</div>
</div>
</div>
);
})}
</div>
{/* View More Button */}
<div className="mt-6 text-center">
<button
onClick={() => window.location.href = '/calendar'}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-gradient-to-r from-orange-400 to-red-500 hover:from-orange-500 hover:to-red-600 transition-colors duration-200"
>
View All Events
<svg className="ml-2 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
</div>
</div>
);
};
export default WhatsHotEvents;