Files
blackcanyontickets/src/pages/events/new.astro
dzinesco 425dfc9348 fix: Remove client-side auth redirects causing dashboard flashing
- Removed checkAuth() function and redirects from dashboard.astro
- Removed checkAuth() function and redirects from events/new.astro
- Updated to use Astro.cookies for better SSR compatibility
- Client-side code now focuses on data loading, not authentication
- Server-side unified auth system handles all protection

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-12 20:40:11 -06:00

564 lines
24 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import Layout from '../../layouts/Layout.astro';
import Navigation from '../../components/Navigation.astro';
import { verifyAuth } from '../../lib/auth';
// Enable server-side rendering for auth checks
export const prerender = false;
// Server-side authentication check
const auth = await verifyAuth(Astro.cookies);
if (!auth) {
return Astro.redirect('/login');
}
---
<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">
<!-- Event Image Upload -->
<div>
<h3 class="text-lg font-medium text-white mb-4">Event Image</h3>
<div id="image-upload-container"></div>
<p class="text-sm text-white/60 mt-2">
Upload a horizontal image. Recommended: 1200×628px. Crop to fit.
</p>
</div>
<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';
import { createElement } from 'react';
import { createRoot } from 'react-dom/client';
import ImageUploadCropper from '../../components/ImageUploadCropper.tsx';
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: string | null = null;
// let selectedAddons: any[] = []; // TODO: Implement addons functionality
let eventImageUrl: string | null = null;
// Load user data (auth already verified server-side)
async function loadUserData() {
const { data: { user: authUser } } = await supabase.auth.getUser();
if (!authUser) {
console.error('No user found despite server-side auth');
return null;
}
// Get user details
const { data: user } = await supabase
.from('users')
.select('name, email, organization_id, role')
.eq('id', authUser.id)
.single();
if (user) {
currentOrganizationId = user.organization_id;
}
return authUser;
}
// 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() {
if (!currentOrganizationId) return;
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') as HTMLInputElement)?.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; // TODO: Use timezone in future
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: string | null = 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,
image_url: eventImageUrl
}
])
.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 Image Upload Component
function initializeImageUpload() {
const container = document.getElementById('image-upload-container');
if (container) {
const root = createRoot(container);
root.render(createElement(ImageUploadCropper, {
onImageChange: (imageUrl) => {
eventImageUrl = imageUrl;
}
}));
}
}
// Initialize (auth already verified server-side)
loadUserData().then(user => {
if (user && currentOrganizationId) {
loadVenues();
}
handleVenueOptionChange(); // Set initial state
initializeImageUpload(); // Initialize image upload
});
</script>