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:
333
src/lib/social-media-generator.ts
Normal file
333
src/lib/social-media-generator.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user