874 lines
No EOL
34 KiB
Vue
874 lines
No EOL
34 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>
|
|
<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 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 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: ''
|
|
}
|
|
})
|
|
|
|
// 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
|
|
}
|
|
} else {
|
|
// Reset series form when no series is selected
|
|
eventForm.series.id = ''
|
|
eventForm.series.title = ''
|
|
eventForm.series.description = ''
|
|
}
|
|
}
|
|
|
|
// 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: ''
|
|
}
|
|
})
|
|
// 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
|
|
}
|
|
|
|
|
|
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 && selectedSeriesId.value) {
|
|
// Series will be handled by the selected series
|
|
} else if (eventForm.series.isSeriesEvent) {
|
|
// For now, series creation requires selecting an existing series
|
|
// Individual series creation is handled through the series management page
|
|
}
|
|
|
|
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: ''
|
|
}
|
|
})
|
|
|
|
// Clear any existing errors
|
|
formErrors.value = []
|
|
fieldErrors.value = {}
|
|
|
|
// Scroll to top
|
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
}
|
|
}
|
|
</script> |