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>
This commit is contained in:
@@ -6,74 +6,113 @@ interface Props {
|
||||
const { eventId } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="bg-white/10 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl mb-8 overflow-hidden">
|
||||
<div class="px-8 py-12 text-white">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<h1 id="event-title" class="text-4xl font-light mb-3 tracking-wide">Loading...</h1>
|
||||
<div class="flex items-center space-x-6 text-slate-200 mb-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="backdrop-blur-xl rounded-3xl shadow-2xl mb-8 overflow-hidden ring-1 transition-all duration-200 hover:shadow-3xl"
|
||||
style="background: var(--glass-bg-lg); border: 1px solid var(--glass-border-subtle); ring-color: var(--glass-ring-dark);"
|
||||
data-theme-card="true">
|
||||
<div class="px-8 py-12" style="color: var(--glass-text-primary);">
|
||||
<div class="flex flex-col lg:flex-row justify-between items-start lg:items-center">
|
||||
<div class="flex-1 mb-6 lg:mb-0">
|
||||
<h1 id="event-title" class="text-3xl font-light mb-2 tracking-wide" style="color: var(--glass-text-primary);">Loading...</h1>
|
||||
<div class="flex flex-wrap items-center gap-4 text-sm mb-3" style="color: var(--glass-text-secondary);">
|
||||
<div class="flex items-center gap-1">
|
||||
<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="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
<span id="event-venue">--</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="flex items-center gap-1">
|
||||
<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 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
<span id="event-date">--</span>
|
||||
</div>
|
||||
</div>
|
||||
<p id="event-description" class="text-slate-300 max-w-2xl leading-relaxed">Loading event details...</p>
|
||||
<div class="max-w-2xl">
|
||||
<p id="event-description" class="text-sm leading-relaxed transition-all duration-300 overflow-hidden body-text" style="color: var(--glass-text-tertiary);">Loading event details...</p>
|
||||
<button id="description-toggle" class="text-xs mt-1 hidden transition-colors hover:opacity-80" style="color: var(--glass-text-accent);">Show more</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-end space-y-3">
|
||||
<div class="flex space-x-3">
|
||||
|
||||
<div class="flex flex-col items-end space-y-4">
|
||||
<!-- Revenue Display -->
|
||||
<div class="text-right">
|
||||
<div class="text-2xl font-semibold" id="total-revenue" style="color: var(--glass-text-primary);">$0</div>
|
||||
<div class="text-xs" style="color: var(--glass-text-secondary);">Total Revenue</div>
|
||||
</div>
|
||||
|
||||
<!-- Compact Button Grid -->
|
||||
<div class="grid grid-cols-3 gap-2 lg:gap-3">
|
||||
<!-- Top Row: Quick Actions -->
|
||||
<a
|
||||
id="preview-link"
|
||||
href="#"
|
||||
target="_blank"
|
||||
class="bg-white/10 hover:bg-white/20 text-white px-6 py-3 rounded-xl font-medium transition-all duration-200 backdrop-blur-sm flex items-center gap-2"
|
||||
class="px-3 py-2 rounded-lg font-medium transition-all duration-200 ease-in-out backdrop-blur-sm flex items-center justify-center gap-1 text-xs hover:scale-[1.02] hover:shadow-lg ring-1 hover:ring-2"
|
||||
style="background: var(--glass-bg-button); color: var(--glass-text-primary); border: 1px solid var(--glass-border); ring-color: var(--glass-ring-dark);"
|
||||
title="Preview Page"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-3 h-3" 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>
|
||||
<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"></path>
|
||||
</svg>
|
||||
Preview Page
|
||||
Preview
|
||||
</a>
|
||||
<button
|
||||
id="embed-code-btn"
|
||||
class="bg-white/10 hover:bg-white/20 text-white px-6 py-3 rounded-xl font-medium transition-all duration-200 backdrop-blur-sm flex items-center gap-2"
|
||||
class="px-3 py-2 rounded-lg font-medium transition-all duration-200 ease-in-out backdrop-blur-sm flex items-center justify-center gap-1 text-xs hover:scale-[1.02] hover:shadow-lg ring-1 hover:ring-2"
|
||||
style="background: var(--glass-bg-button); color: var(--glass-text-primary); border: 1px solid var(--glass-border); ring-color: var(--glass-ring-dark);"
|
||||
title="Get Embed Code"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-3 h-3" 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"></path>
|
||||
</svg>
|
||||
Get Embed Code
|
||||
Embed
|
||||
</button>
|
||||
<button
|
||||
id="edit-event-btn"
|
||||
class="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white px-3 py-2 rounded-lg font-medium transition-all duration-200 ease-in-out flex items-center justify-center gap-1 text-xs hover:scale-[1.02] hover:shadow-lg"
|
||||
title="Edit Event"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
|
||||
<!-- Bottom Row: Tools -->
|
||||
<a
|
||||
href="/scan"
|
||||
class="bg-white/10 hover:bg-white/20 text-white px-6 py-3 rounded-xl font-medium transition-all duration-200 backdrop-blur-sm flex items-center gap-2"
|
||||
class="bg-white/10 hover:bg-white/20 text-white px-3 py-2 rounded-lg font-medium transition-all duration-200 backdrop-blur-sm flex items-center justify-center gap-1 text-xs"
|
||||
title="Scanner"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h4m-6 0h-2v4m0-11v3m0 0h.01M12 12h.01M16 8h4m-6 0h-2v4m0-11v3m0 0h.01M12 12h.01"></path>
|
||||
</svg>
|
||||
Scanner
|
||||
</a>
|
||||
<button
|
||||
id="edit-event-btn"
|
||||
class="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white px-6 py-3 rounded-xl font-medium transition-all duration-200 flex items-center gap-2"
|
||||
<a
|
||||
id="kiosk-link"
|
||||
href="#"
|
||||
class="bg-white/10 hover:bg-white/20 text-white px-3 py-2 rounded-lg font-medium transition-all duration-200 backdrop-blur-sm flex items-center justify-center gap-1 text-xs"
|
||||
title="Sales Kiosk"
|
||||
>
|
||||
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
Edit Event
|
||||
Kiosk
|
||||
</a>
|
||||
<button
|
||||
id="generate-kiosk-pin-btn"
|
||||
class="bg-white/10 hover:bg-white/20 text-white px-3 py-2 rounded-lg font-medium transition-all duration-200 backdrop-blur-sm flex items-center justify-center gap-1 text-xs"
|
||||
title="Generate Kiosk PIN"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
PIN
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-3xl font-semibold" id="total-revenue">$0</div>
|
||||
<div class="text-sm text-slate-300">Total Revenue</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,50 +126,141 @@ const { eventId } = Astro.props;
|
||||
|
||||
async function loadEventHeader() {
|
||||
try {
|
||||
const { createClient } = await import('@supabase/supabase-js');
|
||||
const supabase = createClient(
|
||||
import.meta.env.PUBLIC_SUPABASE_URL,
|
||||
import.meta.env.PUBLIC_SUPABASE_ANON_KEY
|
||||
);
|
||||
const { api } = await import('/src/lib/api-router.js');
|
||||
|
||||
// Load event details and stats using the new API system
|
||||
const result = await api.loadEventPage(eventId);
|
||||
|
||||
if (!result.event) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load event data
|
||||
const { data: event, error } = await supabase
|
||||
.from('events')
|
||||
.select('*')
|
||||
.eq('id', eventId)
|
||||
.single();
|
||||
// Update event details
|
||||
document.getElementById('event-title').textContent = result.event.title;
|
||||
document.getElementById('event-venue').textContent = result.event.venue;
|
||||
|
||||
// Use start_time from database
|
||||
document.getElementById('event-date').textContent = api.formatDate(result.event.start_time);
|
||||
|
||||
// Handle description truncation
|
||||
const descriptionEl = document.getElementById('event-description');
|
||||
const toggleBtn = document.getElementById('description-toggle');
|
||||
const fullDescription = result.event.description;
|
||||
const maxLength = 120; // Show about one line
|
||||
|
||||
if (fullDescription && fullDescription.length > maxLength) {
|
||||
// Set initial truncated text
|
||||
descriptionEl.textContent = fullDescription.substring(0, maxLength) + '...';
|
||||
descriptionEl.dataset.fullText = fullDescription;
|
||||
descriptionEl.dataset.truncatedText = fullDescription.substring(0, maxLength) + '...';
|
||||
descriptionEl.dataset.expanded = 'false';
|
||||
|
||||
// Show toggle button
|
||||
toggleBtn.classList.remove('hidden');
|
||||
|
||||
// Add click handler
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
const isExpanded = descriptionEl.dataset.expanded === 'true';
|
||||
|
||||
if (isExpanded) {
|
||||
descriptionEl.textContent = descriptionEl.dataset.truncatedText;
|
||||
toggleBtn.textContent = 'Show more';
|
||||
descriptionEl.dataset.expanded = 'false';
|
||||
} else {
|
||||
descriptionEl.textContent = descriptionEl.dataset.fullText;
|
||||
toggleBtn.textContent = 'Show less';
|
||||
descriptionEl.dataset.expanded = 'true';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Description is short enough, show full text
|
||||
descriptionEl.textContent = fullDescription;
|
||||
}
|
||||
|
||||
document.getElementById('preview-link').href = `/e/${result.event.slug}`;
|
||||
document.getElementById('kiosk-link').href = `/kiosk/${result.event.slug}`;
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Update UI
|
||||
document.getElementById('event-title').textContent = event.title;
|
||||
document.getElementById('event-venue').textContent = event.venue;
|
||||
document.getElementById('event-date').textContent = new Date(event.date).toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
});
|
||||
document.getElementById('event-description').textContent = event.description;
|
||||
document.getElementById('preview-link').href = `/e/${event.slug}`;
|
||||
|
||||
// Load stats
|
||||
const { data: tickets } = await supabase
|
||||
.from('tickets')
|
||||
.select('price_paid')
|
||||
.eq('event_id', eventId)
|
||||
.eq('status', 'confirmed');
|
||||
|
||||
const totalRevenue = tickets?.reduce((sum, ticket) => sum + ticket.price_paid, 0) || 0;
|
||||
document.getElementById('total-revenue').textContent = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
}).format(totalRevenue / 100);
|
||||
// Update revenue from stats
|
||||
if (result.stats) {
|
||||
document.getElementById('total-revenue').textContent = api.formatCurrency(result.stats.totalRevenue);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading event header:', error);
|
||||
// Error loading event header
|
||||
}
|
||||
}
|
||||
|
||||
// Generate Kiosk PIN functionality
|
||||
document.getElementById('generate-kiosk-pin-btn').addEventListener('click', async () => {
|
||||
if (!confirm('Generate a new PIN for the sales kiosk? This will invalidate any existing PIN.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('generate-kiosk-pin-btn');
|
||||
const originalText = btn.innerHTML;
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
btn.innerHTML = '<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> ...';
|
||||
btn.disabled = true;
|
||||
|
||||
const { api } = await import('/src/lib/api-router.js');
|
||||
const { supabase } = await import('/src/lib/supabase.js');
|
||||
|
||||
// Get auth token
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
if (!session) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
// Generate PIN
|
||||
const response = await fetch('/api/kiosk/generate-pin', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${session.access_token}`
|
||||
},
|
||||
body: JSON.stringify({ eventId })
|
||||
});
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await response.json();
|
||||
} catch (parseError) {
|
||||
throw new Error('Server returned invalid response. Please try again.');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.error || 'Failed to generate PIN');
|
||||
}
|
||||
|
||||
// Send PIN email
|
||||
const emailResponse = await fetch('/api/kiosk/send-pin-email', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
event: result.event,
|
||||
pin: result.pin,
|
||||
email: result.userEmail
|
||||
})
|
||||
});
|
||||
|
||||
const emailResult = await emailResponse.json();
|
||||
|
||||
if (!emailResponse.ok) {
|
||||
alert(`PIN Generated: ${result.pin}\n\nEmail delivery failed. Please note this PIN manually.`);
|
||||
} else {
|
||||
alert(`PIN generated successfully!\n\nA new 4-digit PIN has been sent to ${result.userEmail}.\n\nThe PIN expires in 24 hours.`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
alert('Failed to generate PIN: ' + error.message);
|
||||
} finally {
|
||||
// Restore button
|
||||
btn.innerHTML = originalText;
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user