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

320
src/lib/marketing-kit.ts Normal file
View File

@@ -0,0 +1,320 @@
import { createClient } from '@supabase/supabase-js';
import type { Database } from './database.types';
const supabase = createClient<Database>(
import.meta.env.PUBLIC_SUPABASE_URL,
import.meta.env.PUBLIC_SUPABASE_ANON_KEY
);
export interface MarketingAsset {
id: string;
event_id: string;
asset_type: 'flyer' | 'social_post' | 'email_banner' | 'web_banner' | 'print_ad';
asset_url: string;
asset_data: any;
created_at: string;
}
export interface MarketingKitData {
event: {
id: string;
title: string;
description: string;
date: string;
venue: string;
image_url?: string;
};
assets: MarketingAsset[];
social_links: {
facebook?: string;
twitter?: string;
instagram?: string;
website?: string;
};
}
export interface SocialMediaContent {
platform: 'facebook' | 'twitter' | 'instagram' | 'linkedin';
content: string;
hashtags: string[];
image_url?: string;
}
export interface EmailTemplate {
subject: string;
html_content: string;
text_content: string;
preview_text: string;
}
export async function loadMarketingKit(eventId: string): Promise<MarketingKitData | null> {
try {
// Load event data
const { data: event, error: eventError } = await supabase
.from('events')
.select('id, title, description, date, venue, image_url, social_links')
.eq('id', eventId)
.single();
if (eventError) {
console.error('Error loading event for marketing kit:', eventError);
return null;
}
// Load existing marketing assets
const { data: assets, error: assetsError } = await supabase
.from('marketing_kit_assets')
.select('*')
.eq('event_id', eventId)
.order('created_at', { ascending: false });
if (assetsError) {
console.error('Error loading marketing assets:', assetsError);
return null;
}
return {
event,
assets: assets || [],
social_links: event.social_links || {}
};
} catch (error) {
console.error('Error loading marketing kit:', error);
return null;
}
}
export async function generateMarketingKit(eventId: string): Promise<MarketingKitData | null> {
try {
const response = await fetch(`/api/events/${eventId}/marketing-kit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to generate marketing kit');
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error generating marketing kit:', error);
return null;
}
}
export async function saveMarketingAsset(eventId: string, assetType: string, assetData: any): Promise<MarketingAsset | null> {
try {
const { data: asset, error } = await supabase
.from('marketing_kit_assets')
.insert({
event_id: eventId,
asset_type: assetType,
asset_data: assetData,
asset_url: assetData.url || ''
})
.select()
.single();
if (error) {
console.error('Error saving marketing asset:', error);
return null;
}
return asset;
} catch (error) {
console.error('Error saving marketing asset:', error);
return null;
}
}
export async function updateSocialLinks(eventId: string, socialLinks: Record<string, string>): Promise<boolean> {
try {
const { error } = await supabase
.from('events')
.update({ social_links: socialLinks })
.eq('id', eventId);
if (error) {
console.error('Error updating social links:', error);
return false;
}
return true;
} catch (error) {
console.error('Error updating social links:', error);
return false;
}
}
export function generateSocialMediaContent(event: MarketingKitData['event']): SocialMediaContent[] {
const eventDate = new Date(event.date).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
const baseHashtags = ['#event', '#tickets', '#blackcanyontickets'];
const eventHashtags = event.title.toLowerCase()
.split(' ')
.filter(word => word.length > 3)
.map(word => `#${word.replace(/[^a-zA-Z0-9]/g, '')}`);
const allHashtags = [...baseHashtags, ...eventHashtags.slice(0, 3)];
return [
{
platform: 'facebook',
content: `🎉 Don't miss ${event.title}! Join us on ${eventDate} at ${event.venue}.
${event.description}
Get your tickets now! Link in bio.`,
hashtags: allHashtags,
image_url: event.image_url
},
{
platform: 'twitter',
content: `🎫 ${event.title} - ${eventDate} at ${event.venue}. Get tickets now!`,
hashtags: allHashtags,
image_url: event.image_url
},
{
platform: 'instagram',
content: `${event.title}
📅 ${eventDate}
📍 ${event.venue}
${event.description}
Tickets available now! Link in bio 🎟️`,
hashtags: allHashtags,
image_url: event.image_url
},
{
platform: 'linkedin',
content: `We're excited to announce ${event.title}, taking place on ${eventDate} at ${event.venue}.
${event.description}
Professional networking and entertainment combined. Reserve your spot today.`,
hashtags: allHashtags.slice(0, 3), // LinkedIn prefers fewer hashtags
image_url: event.image_url
}
];
}
export function generateEmailTemplate(event: MarketingKitData['event']): EmailTemplate {
const eventDate = new Date(event.date).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
});
const subject = `Don't Miss ${event.title} - ${eventDate}`;
const previewText = `Join us for an unforgettable experience at ${event.venue}`;
const htmlContent = `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
${event.image_url ? `<img src="${event.image_url}" alt="${event.title}" style="width: 100%; max-width: 600px; height: auto; border-radius: 8px; margin-bottom: 20px;">` : ''}
<h1 style="color: #2563eb; margin-bottom: 20px;">${event.title}</h1>
<div style="background: #f8fafc; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
<h2 style="margin-top: 0; color: #1e293b;">Event Details</h2>
<p><strong>Date:</strong> ${eventDate}</p>
<p><strong>Venue:</strong> ${event.venue}</p>
</div>
<p style="font-size: 16px; margin-bottom: 20px;">${event.description}</p>
<div style="text-align: center; margin: 30px 0;">
<a href="#" style="background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%); color: white; padding: 15px 30px; text-decoration: none; border-radius: 8px; font-weight: bold; display: inline-block;">Get Tickets Now</a>
</div>
<div style="border-top: 1px solid #e2e8f0; padding-top: 20px; margin-top: 30px; text-align: center; color: #64748b; font-size: 14px;">
<p>Powered by Black Canyon Tickets</p>
</div>
</div>
</body>
</html>
`;
const textContent = `
${event.title}
Event Details:
Date: ${eventDate}
Venue: ${event.venue}
${event.description}
Get your tickets now: [TICKET_LINK]
Powered by Black Canyon Tickets
`;
return {
subject,
html_content: htmlContent,
text_content: textContent,
preview_text: previewText
};
}
export function generateFlyerData(event: MarketingKitData['event']): any {
return {
title: event.title,
date: new Date(event.date).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
}),
venue: event.venue,
description: event.description,
image_url: event.image_url,
qr_code_url: `https://portal.blackcanyontickets.com/e/${event.id}`,
template: 'premium',
colors: {
primary: '#2563eb',
secondary: '#7c3aed',
accent: '#06b6d4',
text: '#1e293b'
}
};
}
export async function downloadAsset(assetUrl: string, filename: string): Promise<void> {
try {
const response = await fetch(assetUrl);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('Error downloading asset:', error);
}
}
export function copyToClipboard(text: string): Promise<void> {
return navigator.clipboard.writeText(text);
}