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>
333 lines
9.3 KiB
TypeScript
333 lines
9.3 KiB
TypeScript
import { qrGenerator } from './qr-generator';
|
|
import { canvasImageGenerator } from './canvas-image-generator';
|
|
|
|
interface SocialPost {
|
|
text: string;
|
|
imageUrl: string;
|
|
hashtags: string[];
|
|
dimensions: { width: number; height: number };
|
|
platformSpecific: any;
|
|
}
|
|
|
|
interface EventData {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
venue: string;
|
|
start_time: string;
|
|
end_time: string;
|
|
slug: string;
|
|
image_url?: string;
|
|
social_links?: any;
|
|
website_url?: string;
|
|
organizations: {
|
|
name: string;
|
|
logo?: string;
|
|
social_links?: any;
|
|
website_url?: string;
|
|
};
|
|
}
|
|
|
|
class SocialMediaGenerator {
|
|
private platformDimensions = {
|
|
facebook: { width: 1200, height: 630 },
|
|
instagram: { width: 1080, height: 1080 },
|
|
twitter: { width: 1200, height: 675 },
|
|
linkedin: { width: 1200, height: 627 }
|
|
};
|
|
|
|
private platformLimits = {
|
|
facebook: { textLimit: 2000 },
|
|
instagram: { textLimit: 2200 },
|
|
twitter: { textLimit: 280 },
|
|
linkedin: { textLimit: 3000 }
|
|
};
|
|
|
|
/**
|
|
* Generate social media post for specific platform
|
|
*/
|
|
async generatePost(event: EventData, platform: string): Promise<SocialPost> {
|
|
const dimensions = this.platformDimensions[platform] || this.platformDimensions.facebook;
|
|
|
|
// Generate post text
|
|
const text = this.generatePostText(event, platform);
|
|
|
|
// Generate hashtags
|
|
const hashtags = this.generateHashtags(event, platform);
|
|
|
|
// Generate image
|
|
const imageUrl = await this.generateSocialImage(event, platform, dimensions);
|
|
|
|
// Platform-specific configuration
|
|
const platformSpecific = this.getPlatformSpecificConfig(event, platform);
|
|
|
|
return {
|
|
text,
|
|
imageUrl,
|
|
hashtags,
|
|
dimensions,
|
|
platformSpecific
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate platform-appropriate post text
|
|
*/
|
|
private generatePostText(event: EventData, platform: string): string {
|
|
const eventDate = new Date(event.start_time);
|
|
const formattedDate = eventDate.toLocaleDateString('en-US', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
const formattedTime = eventDate.toLocaleTimeString('en-US', {
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
hour12: true
|
|
});
|
|
|
|
// Get social handles from event or organization
|
|
const socialLinks = event.social_links || event.organizations.social_links || {};
|
|
const orgHandle = this.getSocialHandle(socialLinks, platform);
|
|
|
|
// Get ticket URL
|
|
const ticketUrl = `${import.meta.env.PUBLIC_SITE_URL || 'https://portal.blackcanyontickets.com'}/e/${event.slug}`;
|
|
|
|
const templates = {
|
|
facebook: `🎉 You're Invited: ${event.title}
|
|
|
|
📅 ${formattedDate} at ${formattedTime}
|
|
📍 ${event.venue}
|
|
|
|
${event.description ? event.description.substring(0, 300) + (event.description.length > 300 ? '...' : '') : 'Join us for an unforgettable experience!'}
|
|
|
|
🎫 Get your tickets now: ${ticketUrl}
|
|
|
|
${orgHandle ? `Follow us: ${orgHandle}` : ''}
|
|
|
|
#Events #Tickets #${event.venue.replace(/\s+/g, '')}`,
|
|
|
|
instagram: `✨ ${event.title} ✨
|
|
|
|
📅 ${formattedDate}
|
|
⏰ ${formattedTime}
|
|
📍 ${event.venue}
|
|
|
|
${event.description ? event.description.substring(0, 200) + '...' : 'An experience you won\'t want to miss! 🎭'}
|
|
|
|
Link in bio for tickets 🎫
|
|
👆 or scan the QR code in this post
|
|
|
|
${orgHandle ? `Follow ${orgHandle} for more events` : ''}`,
|
|
|
|
twitter: `🎉 ${event.title}
|
|
|
|
📅 ${formattedDate} • ${formattedTime}
|
|
📍 ${event.venue}
|
|
|
|
🎫 Tickets: ${ticketUrl}
|
|
|
|
${orgHandle || ''}`,
|
|
|
|
linkedin: `Professional Event Announcement: ${event.title}
|
|
|
|
Date: ${formattedDate}
|
|
Time: ${formattedTime}
|
|
Venue: ${event.venue}
|
|
|
|
${event.description ? event.description.substring(0, 400) : 'We invite you to join us for this professional gathering.'}
|
|
|
|
Secure your tickets: ${ticketUrl}
|
|
|
|
${orgHandle ? `Connect with us: ${orgHandle}` : ''}
|
|
|
|
#ProfessionalEvents #Networking #${event.organizations.name.replace(/\s+/g, '')}`
|
|
};
|
|
|
|
const text = templates[platform] || templates.facebook;
|
|
const limit = this.platformLimits[platform]?.textLimit || 2000;
|
|
|
|
return text.length > limit ? text.substring(0, limit - 3) + '...' : text;
|
|
}
|
|
|
|
/**
|
|
* Generate relevant hashtags for the event
|
|
*/
|
|
private generateHashtags(event: EventData, platform: string): string[] {
|
|
const baseHashtags = [
|
|
'Events',
|
|
'Tickets',
|
|
event.organizations.name.replace(/\s+/g, ''),
|
|
event.venue.replace(/\s+/g, ''),
|
|
'EventTickets'
|
|
];
|
|
|
|
// Add date-based hashtags
|
|
const eventDate = new Date(event.start_time);
|
|
const month = eventDate.toLocaleDateString('en-US', { month: 'long' });
|
|
const year = eventDate.getFullYear();
|
|
baseHashtags.push(`${month}${year}`);
|
|
|
|
// Platform-specific hashtag strategies
|
|
const platformHashtags = {
|
|
facebook: [...baseHashtags, 'LocalEvents', 'Community'],
|
|
instagram: [...baseHashtags, 'InstaEvent', 'EventPlanning', 'Memories', 'Experience'],
|
|
twitter: [...baseHashtags.slice(0, 3)], // Twitter users prefer fewer hashtags
|
|
linkedin: [...baseHashtags, 'ProfessionalEvents', 'Networking', 'Business']
|
|
};
|
|
|
|
return platformHashtags[platform] || baseHashtags;
|
|
}
|
|
|
|
/**
|
|
* Generate social media image with event details
|
|
*/
|
|
private async generateSocialImage(
|
|
event: EventData,
|
|
platform: string,
|
|
dimensions: { width: number; height: number }
|
|
): Promise<string> {
|
|
// Generate QR code for the event
|
|
const qrCode = await qrGenerator.generateEventQR(event.slug, {
|
|
size: platform === 'instagram' ? 200 : 150,
|
|
color: { dark: '#000000', light: '#FFFFFF' }
|
|
});
|
|
|
|
// Generate branded image with canvas
|
|
const imageConfig = {
|
|
width: dimensions.width,
|
|
height: dimensions.height,
|
|
platform,
|
|
event,
|
|
qrCode: qrCode.dataUrl,
|
|
backgroundColor: this.getPlatformTheme(platform).backgroundColor,
|
|
textColor: this.getPlatformTheme(platform).textColor,
|
|
accentColor: this.getPlatformTheme(platform).accentColor
|
|
};
|
|
|
|
const imageUrl = await canvasImageGenerator.generateSocialImage(imageConfig);
|
|
|
|
return imageUrl;
|
|
}
|
|
|
|
/**
|
|
* Get platform-specific theme colors
|
|
*/
|
|
private getPlatformTheme(platform: string) {
|
|
const themes = {
|
|
facebook: {
|
|
backgroundColor: ['#1877F2', '#4267B2'],
|
|
textColor: '#FFFFFF',
|
|
accentColor: '#FF6B35'
|
|
},
|
|
instagram: {
|
|
backgroundColor: ['#E4405F', '#F77737', '#FCAF45'],
|
|
textColor: '#FFFFFF',
|
|
accentColor: '#C13584'
|
|
},
|
|
twitter: {
|
|
backgroundColor: ['#1DA1F2', '#0084b4'],
|
|
textColor: '#FFFFFF',
|
|
accentColor: '#FF6B6B'
|
|
},
|
|
linkedin: {
|
|
backgroundColor: ['#0077B5', '#004182'],
|
|
textColor: '#FFFFFF',
|
|
accentColor: '#2867B2'
|
|
}
|
|
};
|
|
|
|
return themes[platform] || themes.facebook;
|
|
}
|
|
|
|
/**
|
|
* Get social handle for platform
|
|
*/
|
|
private getSocialHandle(socialLinks: any, platform: string): string {
|
|
if (!socialLinks || !socialLinks[platform]) {
|
|
return '';
|
|
}
|
|
|
|
const url = socialLinks[platform];
|
|
|
|
// Extract handle from URL
|
|
if (platform === 'twitter') {
|
|
const match = url.match(/twitter\.com\/([^\/]+)/);
|
|
return match ? `@${match[1]}` : '';
|
|
} else if (platform === 'instagram') {
|
|
const match = url.match(/instagram\.com\/([^\/]+)/);
|
|
return match ? `@${match[1]}` : '';
|
|
} else if (platform === 'facebook') {
|
|
const match = url.match(/facebook\.com\/([^\/]+)/);
|
|
return match ? `facebook.com/${match[1]}` : '';
|
|
} else if (platform === 'linkedin') {
|
|
return url;
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
/**
|
|
* Get platform-specific configuration
|
|
*/
|
|
private getPlatformSpecificConfig(event: EventData, platform: string) {
|
|
const ticketUrl = `${import.meta.env.PUBLIC_SITE_URL || 'https://portal.blackcanyontickets.com'}/e/${event.slug}`;
|
|
|
|
return {
|
|
facebook: {
|
|
linkUrl: ticketUrl,
|
|
callToAction: 'Get Tickets',
|
|
eventType: 'ticket_sales'
|
|
},
|
|
instagram: {
|
|
linkInBio: true,
|
|
storyLink: ticketUrl,
|
|
callToAction: 'Link in Bio 👆'
|
|
},
|
|
twitter: {
|
|
linkUrl: ticketUrl,
|
|
tweetIntent: `Check out ${event.title} - ${ticketUrl}`,
|
|
callToAction: 'Get Tickets'
|
|
},
|
|
linkedin: {
|
|
linkUrl: ticketUrl,
|
|
eventType: 'professional',
|
|
callToAction: 'Secure Your Spot'
|
|
}
|
|
}[platform];
|
|
}
|
|
|
|
/**
|
|
* Generate multiple variations of a post
|
|
*/
|
|
async generateVariations(event: EventData, platform: string, count: number = 3): Promise<SocialPost[]> {
|
|
const variations: SocialPost[] = [];
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
// Modify the approach slightly for each variation
|
|
const variation = await this.generatePost(event, platform);
|
|
|
|
// TODO: Implement different text styles, image layouts, etc.
|
|
variations.push(variation);
|
|
}
|
|
|
|
return variations;
|
|
}
|
|
|
|
/**
|
|
* Get optimal posting times for platform
|
|
*/
|
|
getOptimalPostingTimes(platform: string): string[] {
|
|
const times = {
|
|
facebook: ['9:00 AM', '1:00 PM', '7:00 PM'],
|
|
instagram: ['11:00 AM', '2:00 PM', '8:00 PM'],
|
|
twitter: ['8:00 AM', '12:00 PM', '6:00 PM'],
|
|
linkedin: ['8:00 AM', '10:00 AM', '5:00 PM']
|
|
};
|
|
|
|
return times[platform] || times.facebook;
|
|
}
|
|
}
|
|
|
|
export const socialMediaGenerator = new SocialMediaGenerator(); |