Files
blackcanyontickets/src/pages/embed.astro
dzinesco 26a87d0d00 feat: Complete platform enhancement with multi-tenant architecture
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>
2025-07-12 18:21:40 -06:00

391 lines
18 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import Layout from '../layouts/Layout.astro';
---
<Layout title="Embed Script - Black Canyon Tickets">
<div class="min-h-screen bg-gradient-to-br from-slate-50 to-gray-100">
<!-- Sticky Navigation -->
<nav class="sticky top-0 z-50 bg-white/90 backdrop-blur-lg shadow-xl border-b border-slate-200/50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-20">
<div class="flex items-center">
<a href="/dashboard" class="text-xl font-medium text-gray-900">
Black Canyon Tickets
</a>
</div>
<div class="flex items-center">
<a href="/dashboard" class="inline-flex items-center gap-2 text-slate-600 hover:text-slate-900 font-medium transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to Dashboard
</a>
</div>
</div>
</div>
</nav>
<main class="max-w-5xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
<!-- Header -->
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 mb-2">Widget Generator</h1>
<p class="text-gray-600">Create embeddable ticket widgets for your website</p>
</div>
<!-- Configuration Card -->
<div class="bg-white rounded-2xl shadow-xl border border-gray-200 overflow-hidden mb-8">
<div class="px-8 py-6 bg-gray-50 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-900 flex items-center gap-2">
<svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Configuration
</h2>
</div>
<div class="p-8">
<!-- Event Selection -->
<div class="mb-8">
<label for="event-select" class="block text-sm font-semibold text-gray-700 mb-3">
Select Event
</label>
<select
id="event-select"
class="w-full px-4 py-3 border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
>
<option value="">Choose an event...</option>
</select>
</div>
<!-- Widget Options -->
<div id="widget-options" class="hidden">
<div class="grid md:grid-cols-2 gap-8">
<!-- Size Options -->
<div>
<label class="block text-sm font-semibold text-gray-700 mb-4">Widget Size</label>
<div class="space-y-3">
<label class="flex items-center p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
<input type="radio" name="size" value="small" class="text-indigo-600 focus:ring-indigo-500" />
<div class="ml-3">
<div class="font-medium text-gray-900">Small</div>
<div class="text-sm text-gray-500">300 × 400px</div>
</div>
</label>
<label class="flex items-center p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
<input type="radio" name="size" value="medium" class="text-indigo-600 focus:ring-indigo-500" checked />
<div class="ml-3">
<div class="font-medium text-gray-900">Medium</div>
<div class="text-sm text-gray-500">400 × 500px</div>
</div>
</label>
<label class="flex items-center p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
<input type="radio" name="size" value="large" class="text-indigo-600 focus:ring-indigo-500" />
<div class="ml-3">
<div class="font-medium text-gray-900">Large</div>
<div class="text-sm text-gray-500">500 × 600px</div>
</div>
</label>
</div>
</div>
<!-- Theme & Branding -->
<div class="space-y-6">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-4">Theme</label>
<div class="space-y-3">
<label class="flex items-center p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
<input type="radio" name="theme" value="light" class="text-indigo-600 focus:ring-indigo-500" checked />
<div class="ml-3">
<div class="font-medium text-gray-900">Light</div>
<div class="text-sm text-gray-500">Clean, bright appearance</div>
</div>
</label>
<label class="flex items-center p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
<input type="radio" name="theme" value="dark" class="text-indigo-600 focus:ring-indigo-500" />
<div class="ml-3">
<div class="font-medium text-gray-900">Dark</div>
<div class="text-sm text-gray-500">Modern, dark styling</div>
</div>
</label>
</div>
</div>
<div>
<label class="flex items-center p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
<input type="checkbox" id="show-branding" class="text-indigo-600 focus:ring-indigo-500" checked />
<div class="ml-3">
<div class="font-medium text-gray-900">Show Branding</div>
<div class="text-sm text-gray-500">Display "Powered by Black Canyon Tickets"</div>
</div>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Code & Preview Section -->
<div id="embed-code-section" class="hidden grid lg:grid-cols-2 gap-8">
<!-- Embed Code -->
<div class="bg-white rounded-2xl shadow-xl border border-gray-200 overflow-hidden">
<div class="px-6 py-4 bg-gray-50 border-b border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 flex items-center gap-2">
<svg class="w-5 h-5 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
Embed Code
</h3>
</div>
<div class="p-6">
<p class="text-sm text-gray-600 mb-4">
Copy and paste this code into your website:
</p>
<div class="bg-gray-900 rounded-xl p-4 mb-4 overflow-hidden">
<pre id="embed-code" class="text-sm text-green-400 whitespace-pre-wrap overflow-x-auto font-mono"></pre>
</div>
<div class="flex gap-3">
<button
id="copy-code-btn"
class="inline-flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-xl text-sm font-medium transition-colors"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
Copy Code
</button>
<button
id="test-widget-btn"
class="inline-flex items-center gap-2 border border-gray-300 hover:bg-gray-50 text-gray-700 px-4 py-2 rounded-xl text-sm font-medium transition-colors"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
Test Widget
</button>
</div>
</div>
</div>
<!-- Live Preview -->
<div class="bg-white rounded-2xl shadow-xl border border-gray-200 overflow-hidden">
<div class="px-6 py-4 bg-gray-50 border-b border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 flex items-center gap-2">
<svg class="w-5 h-5 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
Live Preview
</h3>
</div>
<div class="p-6">
<p class="text-sm text-gray-600 mb-4">
Preview of your widget:
</p>
<div class="flex justify-center">
<div id="widget-preview" class="inline-block border rounded-xl p-4 bg-gray-50">
<!-- Widget preview will be inserted here -->
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</Layout>
<script>
import { supabase } from '../lib/supabase';
const eventSelect = document.getElementById('event-select') as HTMLSelectElement;
const widgetOptions = document.getElementById('widget-options');
const embedCodeSection = document.getElementById('embed-code-section');
const embedCode = document.getElementById('embed-code');
const copyCodeBtn = document.getElementById('copy-code-btn');
const testWidgetBtn = document.getElementById('test-widget-btn');
const widgetPreview = document.getElementById('widget-preview');
let selectedEvent: any = null;
// Check authentication
async function checkAuth() {
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
window.location.href = '/';
return null;
}
return session;
}
// Load user's events
async function loadEvents() {
try {
const { data: events, error } = await supabase
.from('events')
.select('*')
.order('created_at', { ascending: false });
if (error) throw error;
events.forEach(event => {
const option = document.createElement('option');
option.value = event.id;
option.textContent = event.title;
option.setAttribute('data-event', JSON.stringify(event));
eventSelect.appendChild(option);
});
} catch (error) {
console.error('Error loading events:', error);
}
}
// Generate embed code
function generateEmbedCode() {
if (!selectedEvent) return;
const sizeInputs = document.querySelectorAll('input[name="size"]') as NodeListOf<HTMLInputElement>;
const themeInputs = document.querySelectorAll('input[name="theme"]') as NodeListOf<HTMLInputElement>;
const showBranding = document.getElementById('show-branding') as HTMLInputElement;
const selectedSize = Array.from(sizeInputs).find(input => input.checked)?.value || 'medium';
const selectedTheme = Array.from(themeInputs).find(input => input.checked)?.value || 'light';
const dimensions = {
small: { width: 300, height: 400 },
medium: { width: 400, height: 500 },
large: { width: 500, height: 600 }
}[selectedSize] || { width: 400, height: 500 };
const embedScript = `<!-- Black Canyon Tickets Widget -->
<div id="bct-widget-${selectedEvent.id}" style="width: ${dimensions.width}px; height: ${dimensions.height}px;"></div>
<sc` + `ript>
(function() {
var iframe = document.createElement('iframe');
iframe.src = 'https://portal.blackcanyontickets.com/widget/${selectedEvent.slug}?theme=${selectedTheme}&branding=${showBranding.checked}';
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.border = 'none';
iframe.style.borderRadius = '8px';
iframe.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)';
var container = document.getElementById('bct-widget-${selectedEvent.id}');
if (container) {
container.appendChild(iframe);
}
})();
</sc` + `ript>
<!-- End Black Canyon Tickets Widget -->`;
if (embedCode) {
embedCode.textContent = embedScript;
}
updatePreview();
}
// Update preview
function updatePreview() {
if (!selectedEvent) return;
const sizeInputs = document.querySelectorAll('input[name="size"]') as NodeListOf<HTMLInputElement>;
const themeInputs = document.querySelectorAll('input[name="theme"]') as NodeListOf<HTMLInputElement>;
const showBranding = document.getElementById('show-branding') as HTMLInputElement;
const selectedSize = Array.from(sizeInputs).find(input => input.checked)?.value || 'medium';
const selectedTheme = Array.from(themeInputs).find(input => input.checked)?.value || 'light';
const dimensions = {
small: { width: 300, height: 400 },
medium: { width: 400, height: 500 },
large: { width: 500, height: 600 }
}[selectedSize] || { width: 400, height: 500 };
// Create a simplified preview
const previewHTML = `
<div style="width: ${dimensions.width}px; height: ${dimensions.height}px; border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; background: ${selectedTheme === 'dark' ? '#1f2937' : '#ffffff'}; color: ${selectedTheme === 'dark' ? '#ffffff' : '#1f2937'};">
<h3 style="margin: 0 0 12px 0; font-size: 18px; font-weight: 600;">${selectedEvent.title}</h3>
<p style="margin: 0 0 8px 0; font-size: 14px; opacity: 0.7;">📍 ${selectedEvent.venue}</p>
<p style="margin: 0 0 16px 0; font-size: 14px; opacity: 0.7;">📅 ${new Date(selectedEvent.start_time).toLocaleDateString()}</p>
<div style="border: 1px solid #e5e7eb; border-radius: 4px; padding: 12px; margin-bottom: 16px;">
<p style="margin: 0 0 8px 0; font-size: 14px; font-weight: 500;">Get Your Tickets</p>
<div style="display: flex; gap: 8px; margin-bottom: 8px;">
<select style="flex: 1; padding: 8px; border: 1px solid #e5e7eb; border-radius: 4px; background: ${selectedTheme === 'dark' ? '#374151' : '#ffffff'}; color: ${selectedTheme === 'dark' ? '#ffffff' : '#1f2937'};">
<option>1 Ticket</option>
</select>
</div>
<button style="width: 100%; padding: 12px; background: #4f46e5; color: white; border: none; border-radius: 4px; font-weight: 500; cursor: pointer;">
Purchase Tickets
</button>
</div>
${showBranding.checked ? `<p style="margin: 0; font-size: 12px; opacity: 0.5; text-align: center;">Powered by Black Canyon Tickets</p>` : ''}
</div>
`;
if (widgetPreview) {
widgetPreview.innerHTML = previewHTML;
}
}
// Event listeners
eventSelect.addEventListener('change', (e) => {
const target = e.target as HTMLSelectElement;
if (!target) return;
const selectedOption = target.options[target.selectedIndex];
if (selectedOption.value) {
selectedEvent = JSON.parse(selectedOption.getAttribute('data-event') || '{}');
widgetOptions?.classList.remove('hidden');
embedCodeSection?.classList.remove('hidden');
generateEmbedCode();
} else {
selectedEvent = null;
widgetOptions?.classList.add('hidden');
embedCodeSection?.classList.add('hidden');
}
});
// Listen for option changes
document.addEventListener('change', (e) => {
const target = e.target as HTMLElement;
if (target && 'matches' in target && target.matches('input[name="size"], input[name="theme"], #show-branding')) {
generateEmbedCode();
}
});
copyCodeBtn?.addEventListener('click', () => {
if (embedCode?.textContent) {
navigator.clipboard.writeText(embedCode.textContent).then(() => {
const originalText = copyCodeBtn?.textContent;
if (copyCodeBtn) {
copyCodeBtn.textContent = 'Copied!';
copyCodeBtn.classList.add('bg-green-600');
setTimeout(() => {
if (copyCodeBtn && originalText) {
copyCodeBtn.textContent = originalText;
copyCodeBtn.classList.remove('bg-green-600');
}
}, 2000);
}
});
}
});
testWidgetBtn?.addEventListener('click', () => {
if (selectedEvent) {
window.open(`/e/${selectedEvent.slug}`, '_blank');
}
});
// Initialize
checkAuth().then(session => {
if (session) {
loadEvents();
}
});
</script>