Initial commit - Black Canyon Tickets whitelabel platform
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
377
src/pages/embed.astro
Normal file
377
src/pages/embed.astro
Normal file
@@ -0,0 +1,377 @@
|
||||
---
|
||||
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];
|
||||
|
||||
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 -->`;
|
||||
|
||||
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];
|
||||
|
||||
// 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>
|
||||
`;
|
||||
|
||||
widgetPreview.innerHTML = previewHTML;
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
eventSelect.addEventListener('change', (e) => {
|
||||
const selectedOption = e.target.options[e.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) => {
|
||||
if (e.target.matches('input[name="size"], input[name="theme"], #show-branding')) {
|
||||
generateEmbedCode();
|
||||
}
|
||||
});
|
||||
|
||||
copyCodeBtn.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(embedCode.textContent).then(() => {
|
||||
const originalText = copyCodeBtn.textContent;
|
||||
copyCodeBtn.textContent = 'Copied!';
|
||||
copyCodeBtn.classList.add('bg-green-600');
|
||||
setTimeout(() => {
|
||||
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>
|
||||
Reference in New Issue
Block a user