ghostguild-org/app/pages/admin/events/create.vue

1025 lines
No EOL
41 KiB
Vue

<template>
<div>
<div class="bg-white border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="py-6">
<div class="flex items-center gap-4 mb-2">
<NuxtLink to="/admin/events" class="text-gray-500 hover:text-gray-700">
<Icon name="heroicons:arrow-left" class="w-5 h-5" />
</NuxtLink>
<h1 class="text-2xl font-bold text-gray-900">
{{ editingEvent ? 'Edit Event' : 'Create New Event' }}
</h1>
</div>
<p class="text-gray-600">Fill out the form below to create or update an event</p>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Error Summary -->
<div v-if="formErrors.length > 0" class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<div class="flex">
<Icon name="heroicons:exclamation-circle" class="w-5 h-5 text-red-500 mr-3 mt-0.5" />
<div>
<h3 class="text-sm font-medium text-red-800 mb-2">Please fix the following errors:</h3>
<ul class="text-sm text-red-700 space-y-1">
<li v-for="error in formErrors" :key="error"> {{ error }}</li>
</ul>
</div>
</div>
</div>
<!-- Success Message -->
<div v-if="showSuccessMessage" class="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg">
<div class="flex">
<Icon name="heroicons:check-circle" class="w-5 h-5 text-green-500 mr-3 mt-0.5" />
<div>
<h3 class="text-sm font-medium text-green-800">
{{ editingEvent ? 'Event updated successfully!' : 'Event created successfully!' }}
</h3>
</div>
</div>
</div>
<form @submit.prevent="saveEvent">
<!-- Basic Information -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Basic Information</h2>
<div class="grid grid-cols-1 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Event Title <span class="text-red-500">*</span>
</label>
<input
v-model="eventForm.title"
type="text"
placeholder="Enter a clear, descriptive event title"
required
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
:class="{ 'border-red-300 focus:ring-red-500': fieldErrors.title }"
/>
<p v-if="fieldErrors.title" class="mt-1 text-sm text-red-600">{{ fieldErrors.title }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Feature Image</label>
<ImageUpload v-model="eventForm.featureImage" />
<p class="mt-1 text-sm text-gray-500">Upload a high-quality image (1200x630px recommended) to represent your event</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Event Description <span class="text-red-500">*</span>
</label>
<textarea
v-model="eventForm.description"
placeholder="Provide a clear description of what attendees can expect from this event"
required
rows="4"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
:class="{ 'border-red-300 focus:ring-red-500': fieldErrors.description }"
></textarea>
<p v-if="fieldErrors.description" class="mt-1 text-sm text-red-600">{{ fieldErrors.description }}</p>
<p class="mt-1 text-sm text-gray-500">This will be displayed on the event listing and detail pages</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Additional Content</label>
<textarea
v-model="eventForm.content"
placeholder="Add detailed information, agenda, requirements, or other important details"
rows="6"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
></textarea>
<p class="mt-1 text-sm text-gray-500">Optional: Provide additional context, agenda items, or detailed requirements</p>
</div>
</div>
</div>
<!-- Event Details -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Event Details</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Event Type <span class="text-red-500">*</span>
</label>
<select
v-model="eventForm.eventType"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="community">Community Meetup</option>
<option value="workshop">Workshop</option>
<option value="social">Social Event</option>
<option value="showcase">Showcase</option>
</select>
<p class="mt-1 text-sm text-gray-500">Choose the category that best describes your event</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Location <span class="text-red-500">*</span>
</label>
<input
v-model="eventForm.location"
type="text"
placeholder="e.g., https://zoom.us/j/123... or #channel-name"
required
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
:class="{ 'border-red-300 focus:ring-red-500': fieldErrors.location }"
/>
<p v-if="fieldErrors.location" class="mt-1 text-sm text-red-600">{{ fieldErrors.location }}</p>
<p class="mt-1 text-sm text-gray-500">Enter a video conference link or Slack channel (starting with #)</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Start Date & Time <span class="text-red-500">*</span>
</label>
<NaturalDateInput
v-model="eventForm.startDate"
placeholder="e.g., 'tomorrow at 3pm', 'next Friday at 9am'"
:required="true"
:input-class="{ 'border-red-300 focus:ring-red-500': fieldErrors.startDate }"
/>
<p v-if="fieldErrors.startDate" class="mt-1 text-sm text-red-600">{{ fieldErrors.startDate }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
End Date & Time <span class="text-red-500">*</span>
</label>
<NaturalDateInput
v-model="eventForm.endDate"
placeholder="e.g., 'tomorrow at 5pm', 'next Friday at 11am'"
:required="true"
:input-class="{ 'border-red-300 focus:ring-red-500': fieldErrors.endDate }"
/>
<p v-if="fieldErrors.endDate" class="mt-1 text-sm text-red-600">{{ fieldErrors.endDate }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Max Attendees</label>
<input
v-model="eventForm.maxAttendees"
type="number"
min="1"
placeholder="Leave blank for unlimited"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<p class="mt-1 text-sm text-gray-500">Set a maximum number of attendees (optional)</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Registration Deadline</label>
<NaturalDateInput
v-model="eventForm.registrationDeadline"
placeholder="e.g., 'tomorrow at noon', '1 hour before event'"
/>
<p class="mt-1 text-sm text-gray-500">When should registration close? (optional)</p>
</div>
</div>
</div>
<!-- Target Audience -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Target Audience</h2>
<div>
<label class="block text-sm font-medium text-gray-700 mb-3">Target Circles</label>
<div class="space-y-3">
<label class="flex items-start">
<input
v-model="eventForm.targetCircles"
value="community"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700">Community Circle</span>
<p class="text-xs text-gray-500">New members and those exploring the community</p>
</div>
</label>
<label class="flex items-start">
<input
v-model="eventForm.targetCircles"
value="founder"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700">Founder Circle</span>
<p class="text-xs text-gray-500">Entrepreneurs and business leaders</p>
</div>
</label>
<label class="flex items-start">
<input
v-model="eventForm.targetCircles"
value="practitioner"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700">Practitioner Circle</span>
<p class="text-xs text-gray-500">Experts and professionals sharing knowledge</p>
</div>
</label>
</div>
<p class="mt-2 text-sm text-gray-500">Select which circles this event is most relevant for (leave blank for all circles)</p>
</div>
</div>
<!-- Ticketing -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Ticketing</h2>
<div class="space-y-6">
<label class="flex items-start">
<input
v-model="eventForm.tickets.enabled"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700">Enable Ticketing</span>
<p class="text-xs text-gray-500">Allow ticket sales for this event</p>
</div>
</label>
<div v-if="eventForm.tickets.enabled" class="ml-6 space-y-4 p-4 bg-gray-50 rounded-lg">
<label class="flex items-start">
<input
v-model="eventForm.tickets.public.available"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700">Public Tickets Available</span>
<p class="text-xs text-gray-500">Allow non-members to purchase tickets</p>
</div>
</label>
<div v-if="eventForm.tickets.public.available" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Ticket Name</label>
<input
v-model="eventForm.tickets.public.name"
type="text"
placeholder="e.g., General Admission"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Price (CAD)</label>
<input
v-model="eventForm.tickets.public.price"
type="number"
min="0"
step="0.01"
placeholder="0.00"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<p class="mt-1 text-xs text-gray-500">Set to 0 for free public events</p>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Ticket Description</label>
<textarea
v-model="eventForm.tickets.public.description"
placeholder="What's included with this ticket..."
rows="2"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
></textarea>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Quantity Available</label>
<input
v-model="eventForm.tickets.public.quantity"
type="number"
min="1"
placeholder="Leave blank for unlimited"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Early Bird Price (Optional)</label>
<input
v-model="eventForm.tickets.public.earlyBirdPrice"
type="number"
min="0"
step="0.01"
placeholder="0.00"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
<div v-if="eventForm.tickets.public.earlyBirdPrice > 0">
<label class="block text-sm font-medium text-gray-700 mb-2">Early Bird Deadline</label>
<div class="md:w-1/2">
<NaturalDateInput
v-model="eventForm.tickets.public.earlyBirdDeadline"
placeholder="e.g., '1 week before event', 'next Monday'"
/>
</div>
<p class="mt-1 text-xs text-gray-500">Price increases to regular price after this date</p>
</div>
</div>
</div>
<div class="p-3 bg-blue-50 rounded-lg">
<p class="text-sm text-blue-700">
<strong>Note:</strong> Members always get free access to all events regardless of ticket settings.
</p>
</div>
</div>
</div>
<!-- Series Management -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Series Management</h2>
<div class="space-y-4">
<label class="flex items-start">
<input
v-model="eventForm.series.isSeriesEvent"
type="checkbox"
class="rounded border-gray-300 text-purple-600 focus:ring-purple-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700">Part of Event Series</span>
<p class="text-xs text-gray-500">This event is part of a multi-event series</p>
</div>
</label>
<div v-if="eventForm.series.isSeriesEvent" class="ml-6 space-y-4 p-4 bg-purple-50 rounded-lg border border-purple-200">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Select Series <span class="text-red-500">*</span>
</label>
<div class="flex gap-2">
<select
v-model="selectedSeriesId"
@change="onSeriesSelect"
class="flex-1 border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="">Choose existing series or create new...</option>
<option v-for="series in availableSeries" :key="series.id" :value="series.id">
{{ series.title }} ({{ series.eventCount || 0 }} events)
</option>
</select>
<NuxtLink
to="/admin/series/create"
class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm font-medium whitespace-nowrap"
>
New Series
</NuxtLink>
</div>
<p class="text-xs text-gray-500 mt-1">
Select an existing series or create a new one
</p>
</div>
<div v-if="selectedSeriesId || eventForm.series.id" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Series ID <span class="text-red-500">*</span>
</label>
<input
v-model="eventForm.series.id"
type="text"
placeholder="e.g., coop-dev-fundamentals"
required
:readonly="selectedSeriesId"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
:class="{ 'bg-gray-100': selectedSeriesId }"
@input="!selectedSeriesId && checkExistingSeries()"
/>
<p class="text-xs text-gray-500 mt-1">
{{ selectedSeriesId ? 'From selected series' : 'Unique identifier to group related events (use lowercase with dashes)' }}
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Position in Series <span class="text-red-500">*</span>
</label>
<input
v-model.number="eventForm.series.position"
type="number"
min="1"
required
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
<p class="text-xs text-gray-500 mt-1">Order within the series (1, 2, 3, etc.)</p>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Series Title <span class="text-red-500">*</span>
</label>
<input
v-model="eventForm.series.title"
type="text"
placeholder="e.g., Cooperative Game Development Fundamentals"
required
:readonly="selectedSeriesId"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
:class="{ 'bg-gray-100': selectedSeriesId }"
/>
<p class="text-xs text-gray-500 mt-1">{{ selectedSeriesId ? 'From selected series' : 'Descriptive name for the entire series' }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Series Description <span class="text-red-500">*</span>
</label>
<textarea
v-model="eventForm.series.description"
placeholder="Describe what the series covers and its goals"
required
rows="3"
:readonly="selectedSeriesId"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
:class="{ 'bg-gray-100': selectedSeriesId }"
></textarea>
<p class="text-xs text-gray-500 mt-1">{{ selectedSeriesId ? 'From selected series' : 'Describe what the series covers and its goals' }}</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Series Type</label>
<select
v-model="eventForm.series.type"
:disabled="selectedSeriesId"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
:class="{ 'bg-gray-100': selectedSeriesId }"
>
<option value="workshop_series">Workshop Series</option>
<option value="recurring_meetup">Recurring Meetup</option>
<option value="multi_day">Multi-Day Event</option>
<option value="course">Course</option>
<option value="tournament">Tournament</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Total Events Planned</label>
<input
v-model.number="eventForm.series.totalEvents"
type="number"
min="1"
placeholder="e.g., 4"
:readonly="selectedSeriesId"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
:class="{ 'bg-gray-100': selectedSeriesId }"
/>
<p class="text-xs text-gray-500 mt-1">{{ selectedSeriesId ? 'From selected series' : 'How many events will be in this series?' }}</p>
</div>
</div>
<div v-if="selectedSeriesId" class="p-3 bg-blue-50 rounded-lg">
<p class="text-sm text-blue-700">
<strong>Note:</strong> This event will be added to the existing "{{ eventForm.series.title }}" series.
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Event Settings -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Event Settings</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4">
<label class="flex items-start">
<input
v-model="eventForm.isOnline"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700">Online Event</span>
<p class="text-xs text-gray-500">Event will be conducted virtually</p>
</div>
</label>
<label class="flex items-start">
<input
v-model="eventForm.registrationRequired"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700">Registration Required</span>
<p class="text-xs text-gray-500">Attendees must register before attending</p>
</div>
</label>
</div>
<div class="space-y-4">
<label class="flex items-start">
<input
v-model="eventForm.isVisible"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700">Visible on Public Calendar</span>
<p class="text-xs text-gray-500">Event will appear on the public events page</p>
</div>
</label>
<label class="flex items-start">
<input
v-model="eventForm.isCancelled"
type="checkbox"
class="rounded border-gray-300 text-red-600 focus:ring-red-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700">Event Cancelled</span>
<p class="text-xs text-gray-500">Mark this event as cancelled</p>
</div>
</label>
</div>
</div>
</div>
<!-- Cancellation Message (conditional) -->
<div v-if="eventForm.isCancelled" class="mb-8">
<label class="block text-sm font-medium text-gray-700 mb-2">Cancellation Message</label>
<textarea
v-model="eventForm.cancellationMessage"
placeholder="Explain why the event was cancelled and any next steps..."
rows="3"
class="w-full border border-red-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-red-500 focus:border-transparent"
></textarea>
<p class="text-xs text-gray-500 mt-1">This message will be displayed to users viewing the event page</p>
</div>
<!-- Form Actions -->
<div class="flex justify-between items-center pt-6 border-t border-gray-200">
<NuxtLink
to="/admin/events"
class="px-4 py-2 text-gray-600 hover:text-gray-900 font-medium"
>
Cancel
</NuxtLink>
<div class="flex gap-3">
<button
v-if="!editingEvent"
type="button"
@click="saveAndCreateAnother"
:disabled="creating"
class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
>
{{ creating ? 'Saving...' : 'Save & Create Another' }}
</button>
<button
type="submit"
:disabled="creating"
class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
>
{{ creating ? 'Saving...' : (editingEvent ? 'Update Event' : 'Create Event') }}
</button>
</div>
</div>
</form>
</div>
</div>
</template>
<script setup>
definePageMeta({
layout: 'admin'
})
const route = useRoute()
const router = useRouter()
const creating = ref(false)
const editingEvent = ref(null)
const showSuccessMessage = ref(false)
const formErrors = ref([])
const fieldErrors = ref({})
const seriesExists = ref(false)
const existingSeries = ref(null)
const selectedSeriesId = ref('')
const availableSeries = ref([])
const eventForm = reactive({
title: '',
description: '',
content: '',
featureImage: null,
startDate: '',
endDate: '',
eventType: 'community',
location: '',
isOnline: true,
isVisible: true,
isCancelled: false,
cancellationMessage: '',
targetCircles: [],
maxAttendees: '',
registrationRequired: false,
registrationDeadline: '',
tickets: {
enabled: false,
public: {
available: false,
name: 'Public Ticket',
description: '',
price: 0,
quantity: null,
earlyBirdPrice: null,
earlyBirdDeadline: ''
}
},
series: {
isSeriesEvent: false,
id: '',
title: '',
description: '',
type: 'workshop_series',
position: 1,
totalEvents: null
}
})
// Load available series
onMounted(async () => {
try {
const response = await $fetch('/api/admin/series')
availableSeries.value = response
} catch (error) {
console.error('Failed to load series:', error)
}
})
// Handle series selection
const onSeriesSelect = () => {
if (selectedSeriesId.value) {
const series = availableSeries.value.find(s => s.id === selectedSeriesId.value)
if (series) {
eventForm.series.id = series.id
eventForm.series.title = series.title
eventForm.series.description = series.description
eventForm.series.type = series.type
eventForm.series.totalEvents = series.totalEvents
eventForm.series.position = (series.eventCount || 0) + 1
}
} else {
// Reset series form when no series is selected
eventForm.series.id = ''
eventForm.series.title = ''
eventForm.series.description = ''
eventForm.series.type = 'workshop_series'
eventForm.series.position = 1
eventForm.series.totalEvents = null
}
}
// Check if we're editing an event
if (route.query.edit) {
try {
const response = await $fetch(`/api/admin/events/${route.query.edit}`)
const event = response.data
if (event) {
editingEvent.value = event
Object.assign(eventForm, {
title: event.title,
description: event.description,
content: event.content || '',
featureImage: event.featureImage || null,
startDate: new Date(event.startDate).toISOString().slice(0, 16),
endDate: new Date(event.endDate).toISOString().slice(0, 16),
eventType: event.eventType,
location: event.location || '',
isOnline: event.isOnline,
isVisible: event.isVisible !== undefined ? event.isVisible : true,
isCancelled: event.isCancelled || false,
cancellationMessage: event.cancellationMessage || '',
targetCircles: event.targetCircles || [],
maxAttendees: event.maxAttendees || '',
registrationRequired: event.registrationRequired,
registrationDeadline: event.registrationDeadline ? new Date(event.registrationDeadline).toISOString().slice(0, 16) : '',
tickets: event.tickets || {
enabled: false,
public: {
available: false,
name: 'Public Ticket',
description: '',
price: 0,
quantity: null,
earlyBirdPrice: null,
earlyBirdDeadline: ''
}
},
series: event.series || {
isSeriesEvent: false,
id: '',
title: '',
description: '',
type: 'workshop_series',
position: 1,
totalEvents: null
}
})
// Handle early bird deadline formatting
if (event.tickets?.public?.earlyBirdDeadline) {
eventForm.tickets.public.earlyBirdDeadline = new Date(event.tickets.public.earlyBirdDeadline).toISOString().slice(0, 16)
}
}
} catch (error) {
console.error('Failed to load event for editing:', error)
}
}
// Check if we're duplicating an event
if (route.query.duplicate && process.client) {
const duplicateData = sessionStorage.getItem('duplicateEventData')
if (duplicateData) {
try {
const data = JSON.parse(duplicateData)
Object.assign(eventForm, data)
sessionStorage.removeItem('duplicateEventData')
} catch (error) {
console.error('Failed to load duplicate event data:', error)
}
}
}
// Check if we're creating a series event
if (route.query.series && process.client) {
const seriesData = sessionStorage.getItem('seriesEventData')
if (seriesData) {
try {
const data = JSON.parse(seriesData)
Object.assign(eventForm, data)
sessionStorage.removeItem('seriesEventData')
} catch (error) {
console.error('Failed to load series event data:', error)
}
}
}
const validateForm = () => {
formErrors.value = []
fieldErrors.value = {}
// Required field validation
if (!eventForm.title.trim()) {
formErrors.value.push('Event title is required')
fieldErrors.value.title = 'Please enter an event title'
}
if (!eventForm.description.trim()) {
formErrors.value.push('Event description is required')
fieldErrors.value.description = 'Please provide a description for your event'
}
if (!eventForm.startDate) {
formErrors.value.push('Start date and time is required')
fieldErrors.value.startDate = 'Please select when the event starts'
}
if (!eventForm.endDate) {
formErrors.value.push('End date and time is required')
fieldErrors.value.endDate = 'Please select when the event ends'
}
if (!eventForm.location.trim()) {
formErrors.value.push('Location is required')
fieldErrors.value.location = 'Please enter a location (URL or Slack channel)'
}
// Date validation
if (eventForm.startDate && eventForm.endDate) {
const startDate = new Date(eventForm.startDate)
const endDate = new Date(eventForm.endDate)
if (startDate >= endDate) {
formErrors.value.push('End date must be after start date')
fieldErrors.value.endDate = 'End date must be after the start date'
}
if (startDate < new Date()) {
formErrors.value.push('Start date cannot be in the past')
fieldErrors.value.startDate = 'Event cannot start in the past'
}
}
// Location format validation
if (eventForm.location.trim()) {
const urlPattern = /^https?:\/\/.+/
const slackPattern = /^#[a-zA-Z0-9-_]+$/
if (!urlPattern.test(eventForm.location) && !slackPattern.test(eventForm.location)) {
formErrors.value.push('Location must be a valid URL or Slack channel (starting with #)')
fieldErrors.value.location = 'Enter a video conference link (https://...) or Slack channel (#channel-name)'
}
}
// Registration deadline validation
if (eventForm.registrationDeadline && eventForm.startDate) {
const regDeadline = new Date(eventForm.registrationDeadline)
const startDate = new Date(eventForm.startDate)
if (regDeadline >= startDate) {
formErrors.value.push('Registration deadline must be before the event starts')
fieldErrors.value.registrationDeadline = 'Registration must close before the event starts'
}
}
return formErrors.value.length === 0
}
// Check if a series with this ID already exists
const checkExistingSeries = async () => {
if (!eventForm.series.id || selectedSeriesId.value) {
seriesExists.value = false
existingSeries.value = null
return
}
try {
// First check in standalone series
const standaloneResponse = await $fetch(`/api/admin/series`)
const existingStandalone = standaloneResponse.find(s => s.id === eventForm.series.id)
if (existingStandalone) {
seriesExists.value = true
existingSeries.value = existingStandalone
// Auto-fill series details
if (!eventForm.series.title || eventForm.series.title === '') {
eventForm.series.title = existingStandalone.title
}
if (!eventForm.series.description || eventForm.series.description === '') {
eventForm.series.description = existingStandalone.description
}
if (!eventForm.series.type || eventForm.series.type === 'workshop_series') {
eventForm.series.type = existingStandalone.type
}
if (!eventForm.series.totalEvents || eventForm.series.totalEvents === null) {
eventForm.series.totalEvents = existingStandalone.totalEvents
}
return
}
// Fallback to legacy series check (events with series data)
const legacyResponse = await $fetch(`/api/series/${eventForm.series.id}`)
if (legacyResponse) {
seriesExists.value = true
existingSeries.value = legacyResponse
if (!eventForm.series.title || eventForm.series.title === '') {
eventForm.series.title = legacyResponse.title
}
if (!eventForm.series.description || eventForm.series.description === '') {
eventForm.series.description = legacyResponse.description
}
if (!eventForm.series.type || eventForm.series.type === 'workshop_series') {
eventForm.series.type = legacyResponse.type
}
if (!eventForm.series.totalEvents || eventForm.series.totalEvents === null) {
eventForm.series.totalEvents = legacyResponse.totalEvents
}
}
} catch (error) {
// Series doesn't exist yet
seriesExists.value = false
existingSeries.value = null
}
}
const saveEvent = async (redirect = true) => {
if (!validateForm()) {
// Scroll to top to show errors
window.scrollTo({ top: 0, behavior: 'smooth' })
return false
}
creating.value = true
try {
// If this is a series event and not using an existing series, create the standalone series first
if (eventForm.series.isSeriesEvent && eventForm.series.id && !selectedSeriesId.value) {
try {
await $fetch('/api/admin/series', {
method: 'POST',
body: {
id: eventForm.series.id,
title: eventForm.series.title,
description: eventForm.series.description,
type: eventForm.series.type,
totalEvents: eventForm.series.totalEvents
}
})
} catch (seriesError) {
// Series might already exist, that's ok
if (!seriesError.data?.statusMessage?.includes('already exists')) {
throw seriesError
}
}
}
if (editingEvent.value) {
await $fetch(`/api/admin/events/${editingEvent.value._id}`, {
method: 'PUT',
body: eventForm
})
} else {
await $fetch('/api/admin/events', {
method: 'POST',
body: eventForm
})
}
showSuccessMessage.value = true
setTimeout(() => { showSuccessMessage.value = false }, 5000)
if (redirect) {
setTimeout(() => {
router.push('/admin/events')
}, 1500)
}
return true
} catch (error) {
console.error('Failed to save event:', error)
formErrors.value = [`Failed to ${editingEvent.value ? 'update' : 'create'} event: ${error.data?.statusMessage || error.message}`]
window.scrollTo({ top: 0, behavior: 'smooth' })
return false
} finally {
creating.value = false
}
}
const saveAndCreateAnother = async () => {
const success = await saveEvent(false)
if (success) {
// Reset form for new event
Object.assign(eventForm, {
title: '',
description: '',
content: '',
featureImage: null,
startDate: '',
endDate: '',
eventType: 'community',
location: '',
isOnline: true,
isVisible: true,
isCancelled: false,
cancellationMessage: '',
targetCircles: [],
maxAttendees: '',
registrationRequired: false,
registrationDeadline: '',
tickets: {
enabled: false,
public: {
available: false,
name: 'Public Ticket',
description: '',
price: 0,
quantity: null,
earlyBirdPrice: null,
earlyBirdDeadline: ''
}
},
series: {
isSeriesEvent: false,
id: '',
title: '',
description: '',
type: 'workshop_series',
position: 1,
totalEvents: null
}
})
// Clear any existing errors
formErrors.value = []
fieldErrors.value = {}
// Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' })
}
}
</script>