Major additions: - Territory manager system with application workflow - Custom pricing and page builder with Craft.js - Enhanced Stripe Connect onboarding - CodeReadr QR scanning integration - Kiosk mode for venue sales - Super admin dashboard and analytics - MCP integration for AI-powered operations Infrastructure improvements: - Centralized API client and routing system - Enhanced authentication with organization context - Comprehensive theme management system - Advanced event management with custom tabs - Performance monitoring and accessibility features Database schema updates: - Territory management tables - Custom pages and pricing structures - Kiosk PIN system - Enhanced organization profiles - CodeReadr integration tables 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
317 lines
8.4 KiB
TypeScript
317 lines
8.4 KiB
TypeScript
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
|
|
);
|
|
|
|
// OpenAI configuration
|
|
const OPENAI_API_KEY = import.meta.env.OPENAI_API_KEY;
|
|
const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
|
|
|
|
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;
|
|
start_time: string;
|
|
venue: string;
|
|
image_url?: string;
|
|
};
|
|
assets: MarketingAsset[];
|
|
social_links: {
|
|
facebook?: string;
|
|
twitter?: string;
|
|
instagram?: string;
|
|
website?: string;
|
|
};
|
|
}
|
|
|
|
export interface SocialMediaContent {
|
|
id?: string;
|
|
platform: 'facebook' | 'twitter' | 'instagram' | 'linkedin';
|
|
content: string;
|
|
hashtags: string[];
|
|
image_url?: string;
|
|
tone?: 'professional' | 'casual' | 'exciting' | 'informative' | 'urgent';
|
|
votes?: number;
|
|
generated_at?: 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, start_time, venue, image_url')
|
|
.eq('id', eventId)
|
|
.single();
|
|
|
|
if (eventError) {
|
|
|
|
return null;
|
|
}
|
|
|
|
// Since marketing_kit_assets table doesn't exist, return empty assets
|
|
// This can be implemented later when the table is created
|
|
|
|
return {
|
|
event: {
|
|
...event,
|
|
start_time: event.start_time || '',
|
|
description: event.description || ''
|
|
},
|
|
assets: [], // Empty assets for now
|
|
social_links: {}
|
|
};
|
|
} catch (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) {
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function saveMarketingAsset(eventId: string, assetType: string, assetData: any): Promise<MarketingAsset | null> {
|
|
try {
|
|
// Since marketing_kit_assets table doesn't exist, return a mock asset
|
|
// This can be implemented later when the table is created
|
|
|
|
return {
|
|
id: `temp-${Date.now()}`,
|
|
event_id: eventId,
|
|
asset_type: assetType as any,
|
|
asset_url: assetData.url || '',
|
|
asset_data: assetData,
|
|
created_at: new Date().toISOString()
|
|
};
|
|
} catch (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) {
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
} catch (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) {
|
|
|
|
}
|
|
}
|
|
|
|
export function copyToClipboard(text: string): Promise<void> {
|
|
return navigator.clipboard.writeText(text);
|
|
} |