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

1129 lines
39 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 Agenda -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Event Agenda</h2>
<div class="space-y-3">
<div
v-for="(item, index) in eventForm.agenda"
:key="index"
class="flex gap-2"
>
<input
v-model="eventForm.agenda[index]"
type="text"
placeholder="Enter agenda item (e.g., 'Introduction and welcome - 10 mins')"
class="flex-1 border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<button
type="button"
@click="removeAgendaItem(index)"
class="px-3 py-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
>
<Icon name="heroicons:trash" class="w-5 h-5" />
</button>
</div>
<button
type="button"
@click="addAgendaItem"
class="flex items-center gap-2 px-4 py-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors font-medium"
>
<Icon name="heroicons:plus" class="w-5 h-5" />
Add Agenda Item
</button>
</div>
<p class="mt-2 text-sm text-gray-500">
Add agenda items to help attendees know what to expect during the
event
</p>
</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: "",
agenda: [],
tickets: {
enabled: false,
public: {
available: false,
name: "Public Ticket",
description: "",
price: 0,
quantity: null,
earlyBirdPrice: null,
earlyBirdDeadline: "",
},
},
series: {
isSeriesEvent: false,
id: "",
title: "",
description: "",
},
});
// Agenda management functions
const addAgendaItem = () => {
eventForm.agenda.push("");
};
const removeAgendaItem = (index) => {
eventForm.agenda.splice(index, 1);
};
// 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)
: "",
agenda: event.agenda || [],
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: "",
agenda: [],
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>