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:
523
src/pages/events/new.astro
Normal file
523
src/pages/events/new.astro
Normal 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 675 E Durant Ave 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>
|
||||
Reference in New Issue
Block a user