- 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>
564 lines
24 KiB
Plaintext
564 lines
24 KiB
Plaintext
---
|
||
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 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';
|
||
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> |