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:
320
src/lib/marketing-kit.ts
Normal file
320
src/lib/marketing-kit.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user