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:
2025-07-08 12:31:31 -06:00
commit 997c129383
139 changed files with 60476 additions and 0 deletions

523
src/pages/events/new.astro Normal file
View File

@@ -0,0 +1,523 @@
---
import Layout from '../../layouts/Layout.astro';
import Navigation from '../../components/Navigation.astro';
---
<Layout title="Create Event - Black Canyon Tickets">
<style>
.bg-grid-pattern {
background-image:
linear-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
background-size: 20px 20px;
}
</style>
<div class="min-h-screen bg-gradient-to-br from-indigo-900 via-purple-900 to-slate-900">
<!-- Animated background elements -->
<div class="fixed inset-0 overflow-hidden pointer-events-none">
<div class="absolute -top-40 -right-40 w-80 h-80 bg-gradient-to-br from-purple-600/20 to-pink-600/20 rounded-full blur-3xl animate-pulse"></div>
<div class="absolute -bottom-40 -left-40 w-80 h-80 bg-gradient-to-br from-blue-600/20 to-cyan-600/20 rounded-full blur-3xl animate-pulse"></div>
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-gradient-to-br from-indigo-600/10 to-purple-600/10 rounded-full blur-3xl animate-pulse"></div>
</div>
<!-- Grid pattern overlay -->
<div class="absolute inset-0 bg-grid-pattern opacity-5"></div>
<Navigation
title="Create Event"
showBackLink={true}
backLinkUrl="/dashboard"
backLinkText="← All Events"
/>
<main class="relative max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<!-- Header Section -->
<div class="text-center mb-12">
<h1 class="text-4xl md:text-5xl font-light text-white mb-4 tracking-wide">
Create Your Event
</h1>
<p class="text-xl text-white/80 max-w-2xl mx-auto leading-relaxed">
Set up the foundation for your distinguished event. You'll add tickets, seating, and other details in the next step.
</p>
</div>
<div class="bg-white/10 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl overflow-hidden">
<form id="event-form" class="p-8">
<div class="space-y-8">
<!-- Basic Event Information -->
<div>
<h2 class="text-2xl font-light text-white mb-6">Event Details</h2>
<div class="space-y-6">
<div>
<label for="title" class="block text-sm font-semibold text-white/90 mb-2">Event Title</label>
<input
type="text"
name="title"
id="title"
required
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all duration-200 text-white placeholder-white/50"
placeholder="Summer Gala at The Little Nell"
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-semibold text-white/90 mb-2">Event Date & Time</label>
<div class="space-y-3">
<div>
<label for="event_date" class="block text-xs font-medium text-white/90 mb-1">Date</label>
<input
type="date"
name="event_date"
id="event_date"
required
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all duration-200 text-white placeholder-white/50"
/>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="event_time" class="block text-xs font-medium text-white/90 mb-1">Time</label>
<select
name="event_time"
id="event_time"
required
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all duration-200 text-white placeholder-white/50"
>
<option value="">Select time</option>
<option value="09:00">9:00 AM</option>
<option value="09:30">9:30 AM</option>
<option value="10:00">10:00 AM</option>
<option value="10:30">10:30 AM</option>
<option value="11:00">11:00 AM</option>
<option value="11:30">11:30 AM</option>
<option value="12:00">12:00 PM</option>
<option value="12:30">12:30 PM</option>
<option value="13:00">1:00 PM</option>
<option value="13:30">1:30 PM</option>
<option value="14:00">2:00 PM</option>
<option value="14:30">2:30 PM</option>
<option value="15:00">3:00 PM</option>
<option value="15:30">3:30 PM</option>
<option value="16:00">4:00 PM</option>
<option value="16:30">4:30 PM</option>
<option value="17:00">5:00 PM</option>
<option value="17:30">5:30 PM</option>
<option value="18:00">6:00 PM</option>
<option value="18:30">6:30 PM</option>
<option value="19:00">7:00 PM</option>
<option value="19:30">7:30 PM</option>
<option value="20:00">8:00 PM</option>
<option value="20:30">8:30 PM</option>
<option value="21:00">9:00 PM</option>
<option value="21:30">9:30 PM</option>
<option value="22:00">10:00 PM</option>
<option value="22:30">10:30 PM</option>
<option value="23:00">11:00 PM</option>
</select>
</div>
<div>
<label for="timezone" class="block text-xs font-medium text-white/90 mb-1">Timezone</label>
<select
name="timezone"
id="timezone"
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all duration-200 text-white placeholder-white/50"
>
<option value="America/Denver" selected>Mountain Time (MT)</option>
<option value="America/New_York">Eastern Time (ET)</option>
<option value="America/Chicago">Central Time (CT)</option>
<option value="America/Los_Angeles">Pacific Time (PT)</option>
</select>
</div>
</div>
</div>
</div>
<div>
<label for="seating_type" class="block text-sm font-semibold text-white/90 mb-2">Seating Type</label>
<select
name="seating_type"
id="seating_type"
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all duration-200 text-white placeholder-white/50"
>
<option value="general_admission">General Admission</option>
<option value="assigned_seating">Assigned Seating</option>
<option value="mixed">Mixed (GA + Assigned)</option>
</select>
</div>
</div>
<div>
<label for="description" class="block text-sm font-semibold text-white/90 mb-2">Event Description</label>
<textarea
name="description"
id="description"
rows="4"
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all duration-200 text-white placeholder-white/50"
placeholder="Join us for an elegant evening of exceptional dining, entertainment, and community..."
></textarea>
</div>
</div>
</div>
<!-- Premium Add-ons Section (Coming Soon) -->
<div class="border-t border-white/20 pt-8 opacity-50">
<div class="mb-6">
<button
type="button"
id="addons-toggle"
class="flex items-center justify-between w-full text-left"
>
<div>
<h2 class="text-2xl font-light text-white/40 mb-2">Premium Add-ons</h2>
<p class="text-white/40">Professional features and services (coming soon)</p>
</div>
<svg id="addons-chevron" class="w-6 h-6 text-white/40 transform transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
</div>
<div id="addons-section" class="space-y-4 pointer-events-none hidden">
<div class="bg-white/5 border-2 border-dashed border-white/20 rounded-2xl p-8 text-center">
<div class="text-white/40 mb-4">
<svg class="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" 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"></path>
</svg>
</div>
<h3 class="text-lg font-medium text-white/40 mb-2">Premium Features Coming Soon</h3>
<p class="text-white/40 text-sm max-w-md mx-auto">
Advanced features like seating maps, AI descriptions, premium analytics, and professional scanning tools will be available in future updates.
</p>
</div>
</div>
</div>
<!-- Venue Selection -->
<div class="border-t border-white/20 pt-8">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-light text-white">Venue</h2>
<a
href="/venues"
target="_blank"
class="text-white/70 hover:text-white text-sm font-medium transition-colors duration-200"
>
Manage Venues →
</a>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-semibold text-white/90 mb-3">Choose Venue</label>
<div class="space-y-3">
<label class="flex items-center space-x-3 p-4 border border-white/20 rounded-xl hover:bg-white/5 transition-colors duration-200 cursor-pointer">
<input type="radio" name="venue_option" value="existing" class="text-blue-400 focus:ring-blue-400" checked>
<div>
<span class="font-medium text-white">Select from existing venues</span>
<p class="text-sm text-white/70">Choose from your previously created venues</p>
</div>
</label>
<label class="flex items-center space-x-3 p-4 border border-white/20 rounded-xl hover:bg-white/5 transition-colors duration-200 cursor-pointer">
<input type="radio" name="venue_option" value="custom" class="text-blue-400 focus:ring-blue-400">
<div>
<span class="font-medium text-white">Enter custom venue</span>
<p class="text-sm text-white/70">Specify venue details for this event only</p>
</div>
</label>
</div>
</div>
<!-- Existing Venue Selection -->
<div id="existing-venue-section" class="space-y-4">
<div>
<label for="venue_id" class="block text-sm font-semibold text-white/90 mb-2">Select Venue</label>
<select
name="venue_id"
id="venue_id"
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all duration-200 text-white placeholder-white/50"
>
<option value="">Loading venues...</option>
</select>
</div>
</div>
<!-- Custom Venue Section -->
<div id="custom-venue-section" class="space-y-4 hidden">
<div>
<label for="custom_venue" class="block text-sm font-semibold text-white/90 mb-2">Venue Name & Address</label>
<textarea
name="custom_venue"
id="custom_venue"
rows="3"
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all duration-200 text-white placeholder-white/50"
placeholder="The Little Nell&#10;675 E Durant Ave&#10;Aspen, CO 81611"
></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="flex justify-between items-center pt-8 border-t border-white/20">
<div class="text-sm text-white/70">
<p>Next: Add tickets, configure seating, and customize your event</p>
</div>
<div class="flex space-x-4">
<a
href="/dashboard"
class="px-6 py-3 border border-white/20 text-white/80 rounded-xl hover:bg-white/5 font-medium transition-colors duration-200"
>
Cancel
</a>
<button
type="submit"
class="px-8 py-3 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white rounded-xl font-medium transition-all duration-200 shadow-lg hover:shadow-xl"
>
Create Event & Continue
</button>
</div>
</div>
</form>
</div>
<div id="error-message" class="mt-6 text-sm text-red-400 hidden bg-red-500/10 border border-red-400/20 rounded-xl p-4"></div>
</main>
</div>
</Layout>
<script>
import { supabase } from '../../lib/supabase';
const eventForm = document.getElementById('event-form') as HTMLFormElement;
const errorMessage = document.getElementById('error-message') as HTMLDivElement;
const venueSelect = document.getElementById('venue_id') as HTMLSelectElement;
const existingVenueSection = document.getElementById('existing-venue-section');
const customVenueSection = document.getElementById('custom-venue-section');
let currentOrganizationId = null;
let selectedAddons = [];
// Check authentication
async function checkAuth() {
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
window.location.href = '/';
return null;
}
// Get user details
const { data: user } = await supabase
.from('users')
.select('name, email, organization_id, role')
.eq('id', session.user.id)
.single();
if (user) {
currentOrganizationId = user.organization_id;
}
return session;
}
// Generate slug from title
function generateSlug(title: string): string {
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
// Premium add-ons are disabled for now
// This functionality will be enabled in future updates
// Load available venues
async function loadVenues() {
try {
const { data: venues, error } = await supabase
.from('venues')
.select('id, name, address')
.eq('organization_id', currentOrganizationId)
.order('name');
if (error) throw error;
venueSelect.innerHTML = '<option value="">Select a venue...</option>';
if (venues.length === 0) {
venueSelect.innerHTML = '<option value="">No venues created yet</option>';
} else {
venues.forEach(venue => {
const option = document.createElement('option');
option.value = venue.id;
option.textContent = venue.name + (venue.address ? ` - ${venue.address}` : '');
venueSelect.appendChild(option);
});
}
} catch (error) {
// Handle venue loading errors gracefully
venueSelect.innerHTML = '<option value="">Unable to load venues</option>';
}
}
// Handle venue option change
function handleVenueOptionChange() {
const venueOption = document.querySelector('input[name="venue_option"]:checked')?.value;
if (venueOption === 'existing') {
existingVenueSection.classList.remove('hidden');
customVenueSection.classList.add('hidden');
} else {
existingVenueSection.classList.add('hidden');
customVenueSection.classList.remove('hidden');
}
}
// Event form submission
eventForm.addEventListener('submit', async (e) => {
e.preventDefault();
try {
errorMessage.classList.add('hidden');
const formData = new FormData(eventForm);
const title = formData.get('title') as string;
const eventDate = formData.get('event_date') as string;
const eventTime = formData.get('event_time') as string;
const timezone = formData.get('timezone') as string;
const description = formData.get('description') as string;
const seatingType = formData.get('seating_type') as string;
const venueOption = formData.get('venue_option') as string;
// Combine date and time
if (!eventDate || !eventTime) {
throw new Error('Please select both date and time for your event');
}
const startTime = `${eventDate}T${eventTime}:00`;
const slug = generateSlug(title);
// Get current user and organization
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error('Not authenticated');
let organizationId = currentOrganizationId;
if (!organizationId) {
// Create a default organization for the user
const { data: org, error: orgError } = await supabase
.from('organizations')
.insert([
{ name: `${user.user_metadata?.name || user.email}'s Organization` }
])
.select()
.single();
if (orgError) {
throw new Error('Unable to create organization. Please contact support.');
}
organizationId = org.id;
// Update user with organization_id
const { error: updateError } = await supabase
.from('users')
.update({ organization_id: organizationId })
.eq('id', user.id);
if (updateError) {
// Silently handle user update errors
}
// Update current state
currentOrganizationId = organizationId;
}
// Determine venue information
let venue, venueId = null;
if (venueOption === 'existing') {
venueId = formData.get('venue_id') as string;
if (!venueId) {
throw new Error('Please select a venue or choose custom venue option');
}
// Get venue name for display
const { data: venueData } = await supabase
.from('venues')
.select('name')
.eq('id', venueId)
.single();
venue = venueData?.name || 'Selected Venue';
} else {
venue = formData.get('custom_venue') as string;
if (!venue) {
throw new Error('Please enter venue information');
}
}
// Create the event
const { data: event, error: eventError } = await supabase
.from('events')
.insert([
{
title,
slug,
venue,
venue_id: venueId,
start_time: startTime,
description,
created_by: user.id,
organization_id: organizationId,
seating_type: seatingType
}
])
.select()
.single();
if (eventError) throw eventError;
// Premium add-ons will be handled in future updates
// Redirect to the event dashboard
window.location.href = `/events/${event.id}/manage`;
} catch (error) {
// Handle errors gracefully without exposing details
errorMessage.textContent = 'An error occurred creating the event. Please try again.';
errorMessage.classList.remove('hidden');
}
});
// Toggle premium addons section
const addonsToggle = document.getElementById('addons-toggle');
const addonsSection = document.getElementById('addons-section');
const addonsChevron = document.getElementById('addons-chevron');
addonsToggle?.addEventListener('click', () => {
const isHidden = addonsSection.classList.contains('hidden');
if (isHidden) {
addonsSection.classList.remove('hidden');
addonsChevron.classList.add('rotate-180');
} else {
addonsSection.classList.add('hidden');
addonsChevron.classList.remove('rotate-180');
}
});
// Event listeners
document.querySelectorAll('input[name="venue_option"]').forEach(radio => {
radio.addEventListener('change', handleVenueOptionChange);
});
// Initialize
checkAuth().then(session => {
if (session && currentOrganizationId) {
loadVenues();
}
handleVenueOptionChange(); // Set initial state
});
</script>