Files
blackcanyontickets/src/components/EventHeader.astro
dzinesco a049472a13 fix: resolve ticket modal issues and improve functionality
- Fixed modal background opacity from 0.5 to 0.75 for better visibility
- Fixed X button close functionality in TicketTypeModal
- Resolved CORS issues by removing credentials: 'include' from Supabase client
- Fixed onSave callback signature mismatch in TicketsTab component
- Removed old initEmbedModal function references causing JavaScript errors
- Added comprehensive Playwright tests for ticket button functionality
- Verified modal works correctly in both light and dark modes

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 08:45:39 -06:00

504 lines
21 KiB
Plaintext

---
import SimpleEmbedTest from './SimpleEmbedTest.tsx';
interface Props {
eventId: string;
}
const { eventId } = Astro.props;
---
<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 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>
<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-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="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-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
</a>
<button
id="embed-code-btn"
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-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>
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-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-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>
<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-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>
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>
</div>
</div>
</div>
<!-- Simple Embed Test -->
<SimpleEmbedTest
client:load
eventId={eventId}
/>
<script define:vars={{ eventId }}>
// Initialize event header when page loads
document.addEventListener('DOMContentLoaded', async () => {
await loadEventHeader();
});
// Format currency helper function
function formatCurrency(amountInCents) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(amountInCents / 100);
}
// Format date helper function
function formatDate(dateString) {
return new Date(dateString).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
});
}
async function loadEventHeader() {
try {
// Load event details using server-side API
const eventResponse = await fetch(`/api/events/${eventId}`);
if (!eventResponse.ok) {
throw new Error('Failed to load event');
}
const event = await eventResponse.json();
// Load event stats using server-side API
const statsResponse = await fetch(`/api/events/${eventId}/stats`);
let stats = null;
if (statsResponse.ok) {
stats = await statsResponse.json();
}
// Update event details
document.getElementById('event-title').textContent = event.title;
document.getElementById('event-venue').textContent = event.venue;
// Use start_time from database
document.getElementById('event-date').textContent = formatDate(event.start_time);
// Handle description truncation
const descriptionEl = document.getElementById('event-description');
const toggleBtn = document.getElementById('description-toggle');
const fullDescription = 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/${event.slug}`;
document.getElementById('kiosk-link').href = `/kiosk/${event.slug}`;
// Update revenue from stats
if (stats) {
document.getElementById('total-revenue').textContent = formatCurrency(stats.totalRevenue);
}
} catch (error) {
console.error('Error loading event header:', error);
}
}
// Event handlers will be added after functions are defined
// Function to show edit event modal
function showEditEventModal(event) {
// Create modal backdrop
const backdrop = document.createElement('div');
backdrop.className = 'fixed inset-0 z-50 bg-black bg-opacity-50 flex items-center justify-center p-4';
backdrop.style.display = 'flex';
// Create modal content
const modal = document.createElement('div');
modal.className = 'bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto';
// Format date for input (YYYY-MM-DD)
const eventDate = new Date(event.start_time);
const formattedDate = eventDate.toISOString().split('T')[0];
// Format time for input (HH:MM)
const formattedTime = eventDate.toTimeString().slice(0, 5);
modal.innerHTML = `
<div class="p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-semibold text-gray-900">Edit Event</h2>
<button id="close-edit-modal" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<form id="edit-event-form" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Event Title</label>
<input type="text" id="edit-title" value="${event.title || ''}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Venue</label>
<input type="text" id="edit-venue" value="${event.venue || ''}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Date</label>
<input type="date" id="edit-date" value="${formattedDate}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Time</label>
<input type="time" id="edit-time" value="${formattedTime}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Description</label>
<textarea id="edit-description" rows="4"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent">${event.description || ''}</textarea>
</div>
<div class="flex justify-end space-x-3 pt-4">
<button type="button" id="cancel-edit"
class="px-4 py-2 text-gray-600 hover:text-gray-800">
Cancel
</button>
<button type="submit" id="save-edit"
class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:ring-2 focus:ring-blue-500">
Save Changes
</button>
</div>
</form>
</div>
`;
backdrop.appendChild(modal);
document.body.appendChild(backdrop);
// Add event listeners
document.getElementById('close-edit-modal').addEventListener('click', () => {
document.body.removeChild(backdrop);
});
document.getElementById('cancel-edit').addEventListener('click', () => {
document.body.removeChild(backdrop);
});
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop) {
document.body.removeChild(backdrop);
}
});
// Handle form submission
document.getElementById('edit-event-form').addEventListener('submit', async (e) => {
e.preventDefault();
const submitBtn = document.getElementById('save-edit');
const originalText = submitBtn.textContent;
try {
// Show loading state
submitBtn.textContent = 'Saving...';
submitBtn.disabled = true;
// Prepare update data
const title = document.getElementById('edit-title').value.trim();
const venue = document.getElementById('edit-venue').value.trim();
const date = document.getElementById('edit-date').value;
const time = document.getElementById('edit-time').value;
const description = document.getElementById('edit-description').value.trim();
if (!title || !venue || !date || !time) {
throw new Error('Please fill in all required fields');
}
// Combine date and time
const startTime = new Date(date + 'T' + time).toISOString();
const updateData = {
title,
venue,
start_time: startTime,
description
};
// Send update request
const response = await fetch(`/api/events/${eventId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updateData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to update event');
}
// Close modal
document.body.removeChild(backdrop);
// Reload the page to show updated information
window.location.reload();
} catch (error) {
alert('Failed to update event: ' + error.message);
submitBtn.textContent = originalText;
submitBtn.disabled = false;
}
});
}
// Copy to clipboard function
window.copyToClipboard = function(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => {
// Show temporary success message
const btn = document.activeElement;
const originalText = btn.textContent;
btn.textContent = 'Copied!';
btn.className = btn.className.replace('bg-blue-600 hover:bg-blue-700', 'bg-green-600');
setTimeout(() => {
btn.textContent = originalText;
btn.className = btn.className.replace('bg-green-600', 'bg-blue-600 hover:bg-blue-700');
}, 2000);
});
} else {
// Fallback for older browsers
try {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
alert('Copied to clipboard!');
} else {
alert('Could not copy to clipboard');
}
} catch (err) {
alert('Copy not supported in this browser');
}
}
};
// Generate Kiosk PIN functionality - temporarily disabled due to import issues
document.getElementById('generate-kiosk-pin-btn').addEventListener('click', async () => {
alert('Kiosk PIN generation is temporarily disabled. Please use the admin panel for PIN management.');
});
// Add event listeners after DOM is ready and functions are defined
function addEventListeners() {
console.log('[DEBUG] Adding event listeners...');
// Edit Event Button functionality
const editBtn = document.getElementById('edit-event-btn');
console.log('[DEBUG] Edit button found:', !!editBtn);
if (editBtn) {
console.log('[DEBUG] Adding click listener to edit button');
editBtn.addEventListener('click', async () => {
console.log('[DEBUG] Edit button clicked!');
alert('Edit button clicked - this proves the event listener is working!');
// Load current event data and show edit modal
try {
console.log('[DEBUG] Fetching event data...');
// Get eventId from the URL instead of the variable
const urlParts = window.location.pathname.split('/');
const currentEventId = urlParts[urlParts.indexOf('events') + 1];
console.log('[DEBUG] Event ID from URL:', currentEventId);
const eventResponse = await fetch(`/api/events/${currentEventId}`);
if (!eventResponse.ok) {
throw new Error('Failed to load event details');
}
const event = await eventResponse.json();
console.log('[DEBUG] Event data loaded:', event.title);
showEditEventModal(event);
} catch (error) {
console.error('[DEBUG] Error:', error);
alert('Failed to load event details: ' + error.message);
}
});
} else {
console.error('[DEBUG] Edit button not found!');
}
// Embed Code Button functionality
const embedBtn = document.getElementById('embed-code-btn');
if (embedBtn) {
embedBtn.addEventListener('click', () => {
// Get event details for the embed modal
const previewLink = document.getElementById('preview-link').href;
const eventSlug = previewLink ? previewLink.split('/e/')[1] : 'loading';
// Get eventId from the URL
const urlParts = window.location.pathname.split('/');
const currentEventId = urlParts[urlParts.indexOf('events') + 1];
// Show embed modal using React component
if (window.openEmbedModal) {
window.openEmbedModal(currentEventId, eventSlug);
} else {
// Fallback: show simple alert for debugging
alert(`Embed Modal Debug:\nEvent ID: ${currentEventId}\nEvent Slug: ${eventSlug}\nwindow.openEmbedModal: ${typeof window.openEmbedModal}`);
console.log('Embed button clicked but window.openEmbedModal not available');
console.log('Available window properties:', Object.keys(window).filter(k => k.includes('embed')));
}
});
}
}
// Add event listeners after DOM is loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
addEventListeners();
});
} else {
addEventListeners();
}
</script>