1218 lines
34 KiB
Vue
1218 lines
34 KiB
Vue
<template>
|
|
<div class="create-form">
|
|
<div class="page-header">
|
|
<div class="header-row">
|
|
<NuxtLink to="/admin/events" class="back-link">← Events</NuxtLink>
|
|
<h1>{{ editingEvent ? "Edit Event" : "Create Event" }}</h1>
|
|
<p>Fill out the form below to create or update an event</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-body">
|
|
<!-- Error Summary -->
|
|
<div v-if="formErrors.length > 0" class="error-box">
|
|
<h3>Please fix the following errors:</h3>
|
|
<ul>
|
|
<li v-for="error in formErrors" :key="error">{{ error }}</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Success Message -->
|
|
<div v-if="showSuccessMessage" class="success-box">
|
|
{{
|
|
editingEvent
|
|
? "Event updated successfully!"
|
|
: "Event created successfully!"
|
|
}}
|
|
</div>
|
|
|
|
<form @submit.prevent="saveEvent">
|
|
<!-- Basic Information -->
|
|
<div class="form-section">
|
|
<h2 class="section-heading">Basic Information</h2>
|
|
|
|
<div class="field">
|
|
<label> Event Title <span class="required">*</span> </label>
|
|
<UInput
|
|
v-model="eventForm.title"
|
|
placeholder="Enter a clear, descriptive event title"
|
|
required
|
|
:color="fieldErrors.title ? 'error' : undefined"
|
|
class="w-full"
|
|
/>
|
|
<p v-if="fieldErrors.title" class="field-error">
|
|
{{ fieldErrors.title }}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Feature Image</label>
|
|
<ImageUpload v-model="eventForm.featureImage" />
|
|
<p class="help-text">
|
|
Upload a high-quality image (1200x630px recommended) to represent
|
|
your event
|
|
</p>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label> Event Description <span class="required">*</span> </label>
|
|
<UTextarea
|
|
v-model="eventForm.description"
|
|
placeholder="Provide a clear description of what attendees can expect from this event"
|
|
required
|
|
:rows="4"
|
|
:color="fieldErrors.description ? 'error' : undefined"
|
|
class="w-full"
|
|
/>
|
|
<p v-if="fieldErrors.description" class="field-error">
|
|
{{ fieldErrors.description }}
|
|
</p>
|
|
<p class="help-text">
|
|
This will be displayed on the event listing and detail pages
|
|
</p>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Additional Content</label>
|
|
<UTextarea
|
|
v-model="eventForm.content"
|
|
placeholder="Add detailed information, agenda, requirements, or other important details"
|
|
:rows="6"
|
|
class="w-full"
|
|
/>
|
|
<p class="help-text">
|
|
Optional: Provide additional context, agenda items, or detailed
|
|
requirements
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Event Details -->
|
|
<div class="form-section">
|
|
<h2 class="section-heading">Event Details</h2>
|
|
|
|
<div class="form-grid">
|
|
<div class="field">
|
|
<label> Event Type <span class="required">*</span> </label>
|
|
<USelect
|
|
v-model="eventForm.eventType"
|
|
aria-label="Event type"
|
|
:items="[
|
|
{ label: 'Community Meetup', value: 'community' },
|
|
{ label: 'Workshop', value: 'workshop' },
|
|
{ label: 'Social Event', value: 'social' },
|
|
{ label: 'Showcase', value: 'showcase' },
|
|
]"
|
|
class="w-full"
|
|
/>
|
|
<p class="help-text">
|
|
Choose the category that best describes your event
|
|
</p>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label> Location <span class="required">*</span> </label>
|
|
<UInput
|
|
v-model="eventForm.location"
|
|
placeholder="e.g., https://zoom.us/j/123... or #channel-name"
|
|
required
|
|
:color="fieldErrors.location ? 'error' : undefined"
|
|
class="w-full"
|
|
/>
|
|
<p v-if="fieldErrors.location" class="field-error">
|
|
{{ fieldErrors.location }}
|
|
</p>
|
|
<p class="help-text">
|
|
Enter a video conference link or Slack channel (starting with #)
|
|
</p>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label> Start Date & Time <span class="required">*</span> </label>
|
|
<NaturalDateInput
|
|
v-model="eventForm.startDate"
|
|
placeholder="e.g., 'tomorrow at 3pm', 'next Friday at 9am'"
|
|
:required="true"
|
|
/>
|
|
<p v-if="fieldErrors.startDate" class="field-error">
|
|
{{ fieldErrors.startDate }}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label> End Date & Time <span class="required">*</span> </label>
|
|
<NaturalDateInput
|
|
v-model="eventForm.endDate"
|
|
placeholder="e.g., 'tomorrow at 5pm', 'next Friday at 11am'"
|
|
:required="true"
|
|
/>
|
|
<p v-if="fieldErrors.endDate" class="field-error">
|
|
{{ fieldErrors.endDate }}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Max Attendees</label>
|
|
<UInput
|
|
v-model="eventForm.maxAttendees"
|
|
type="number"
|
|
min="1"
|
|
placeholder="Leave blank for unlimited"
|
|
class="w-full"
|
|
/>
|
|
<p class="help-text">
|
|
Set a maximum number of attendees (optional)
|
|
</p>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Registration Deadline</label>
|
|
<NaturalDateInput
|
|
v-model="eventForm.registrationDeadline"
|
|
placeholder="e.g., 'tomorrow at noon', '1 hour before event'"
|
|
/>
|
|
<p class="help-text">
|
|
When should registration close? (optional)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Target Audience -->
|
|
<div class="form-section">
|
|
<h2 class="section-heading">Target Audience</h2>
|
|
|
|
<div class="field">
|
|
<label>Target Circles</label>
|
|
<div class="check-group">
|
|
<label class="check-label">
|
|
<input
|
|
v-model="eventForm.targetCircles"
|
|
value="community"
|
|
type="checkbox"
|
|
/>
|
|
<div>
|
|
<strong>Community Circle</strong>
|
|
<span class="help-text">
|
|
New members and those exploring the community
|
|
</span>
|
|
</div>
|
|
</label>
|
|
<label class="check-label">
|
|
<input
|
|
v-model="eventForm.targetCircles"
|
|
value="founder"
|
|
type="checkbox"
|
|
/>
|
|
<div>
|
|
<strong>Founder Circle</strong>
|
|
<span class="help-text">
|
|
Entrepreneurs and business leaders
|
|
</span>
|
|
</div>
|
|
</label>
|
|
<label class="check-label">
|
|
<input
|
|
v-model="eventForm.targetCircles"
|
|
value="practitioner"
|
|
type="checkbox"
|
|
/>
|
|
<div>
|
|
<strong>Practitioner Circle</strong>
|
|
<span class="help-text">
|
|
Experts and professionals sharing knowledge
|
|
</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
<p class="help-text">
|
|
Select which circles this event is most relevant for (leave blank
|
|
for all circles)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tags -->
|
|
<div class="form-section">
|
|
<h2 class="section-heading">Tags</h2>
|
|
|
|
<div class="field">
|
|
<label>Event Tags</label>
|
|
<USelectMenu
|
|
v-model="eventForm.tags"
|
|
:items="tagOptions"
|
|
value-key="value"
|
|
multiple
|
|
placeholder="Select tags..."
|
|
class="w-full"
|
|
/>
|
|
<p class="help-text">
|
|
Tag this event to help with discovery and recommendations
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ticketing -->
|
|
<div class="form-section">
|
|
<h2 class="section-heading">Ticketing</h2>
|
|
|
|
<label class="check-label">
|
|
<input v-model="eventForm.tickets.enabled" type="checkbox" />
|
|
<div>
|
|
<strong>Enable Ticketing</strong>
|
|
<span class="help-text"> Allow ticket sales for this event </span>
|
|
</div>
|
|
</label>
|
|
|
|
<div v-if="eventForm.tickets.enabled" class="nested-section">
|
|
<label class="check-label">
|
|
<input
|
|
v-model="eventForm.tickets.public.available"
|
|
type="checkbox"
|
|
/>
|
|
<div>
|
|
<strong>Public Tickets Available</strong>
|
|
<span class="help-text">
|
|
Allow non-members to purchase tickets
|
|
</span>
|
|
</div>
|
|
</label>
|
|
|
|
<div v-if="eventForm.tickets.public.available">
|
|
<div class="form-grid">
|
|
<div class="field">
|
|
<label>Ticket Name</label>
|
|
<UInput
|
|
v-model="eventForm.tickets.public.name"
|
|
placeholder="e.g., General Admission"
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Price (CAD)</label>
|
|
<UInput
|
|
v-model="eventForm.tickets.public.price"
|
|
type="number"
|
|
min="0"
|
|
step="0.01"
|
|
placeholder="0.00"
|
|
class="w-full"
|
|
/>
|
|
<p class="help-text">Set to 0 for free public events</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Ticket Description</label>
|
|
<UTextarea
|
|
v-model="eventForm.tickets.public.description"
|
|
placeholder="What's included with this ticket..."
|
|
:rows="2"
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-grid">
|
|
<div class="field">
|
|
<label>Quantity Available</label>
|
|
<UInput
|
|
v-model="eventForm.tickets.public.quantity"
|
|
type="number"
|
|
min="1"
|
|
placeholder="Leave blank for unlimited"
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Early Bird Price (Optional)</label>
|
|
<UInput
|
|
v-model="eventForm.tickets.public.earlyBirdPrice"
|
|
type="number"
|
|
min="0"
|
|
step="0.01"
|
|
placeholder="0.00"
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="eventForm.tickets.public.earlyBirdPrice > 0"
|
|
class="field"
|
|
>
|
|
<label>Early Bird Deadline</label>
|
|
<NaturalDateInput
|
|
v-model="eventForm.tickets.public.earlyBirdDeadline"
|
|
placeholder="e.g., '1 week before event', 'next Monday'"
|
|
/>
|
|
<p class="help-text">
|
|
Price increases to regular price after this date
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="note-box">
|
|
<strong>Note:</strong> Members always get free access to all events
|
|
regardless of ticket settings.
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Series Management -->
|
|
<div class="form-section">
|
|
<h2 class="section-heading">Series Management</h2>
|
|
|
|
<label class="check-label">
|
|
<input v-model="eventForm.series.isSeriesEvent" type="checkbox" />
|
|
<div>
|
|
<strong>Part of Event Series</strong>
|
|
<span class="help-text">
|
|
This event is part of a multi-event series
|
|
</span>
|
|
</div>
|
|
</label>
|
|
|
|
<div v-if="eventForm.series.isSeriesEvent" class="nested-section">
|
|
<div class="field">
|
|
<label> Select Series <span class="required">*</span> </label>
|
|
<div class="series-select-row">
|
|
<USelect
|
|
v-model="selectedSeriesId"
|
|
aria-label="Select series"
|
|
@update:model-value="onSeriesSelect"
|
|
:items="
|
|
availableSeries.map((series) => ({
|
|
label: `${series.title} (${series.eventCount || 0} events)`,
|
|
value: series.id,
|
|
}))
|
|
"
|
|
placeholder="Choose existing series or create new..."
|
|
value-key="value"
|
|
class="w-full"
|
|
/>
|
|
<NuxtLink to="/admin/series/create" class="btn btn-primary">
|
|
New Series
|
|
</NuxtLink>
|
|
</div>
|
|
<p class="help-text">
|
|
Select an existing series or create a new one
|
|
</p>
|
|
</div>
|
|
|
|
<div v-if="selectedSeriesId || eventForm.series.id">
|
|
<div class="field">
|
|
<label> Series Title <span class="required">*</span> </label>
|
|
<UInput
|
|
v-model="eventForm.series.title"
|
|
placeholder="e.g., Cooperative Game Development Fundamentals"
|
|
required
|
|
:readonly="selectedSeriesId"
|
|
class="w-full"
|
|
/>
|
|
<p class="help-text">
|
|
{{
|
|
selectedSeriesId
|
|
? "From selected series"
|
|
: "Descriptive name for the entire series"
|
|
}}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>
|
|
Series Description <span class="required">*</span>
|
|
</label>
|
|
<UTextarea
|
|
v-model="eventForm.series.description"
|
|
placeholder="Describe what the series covers and its goals"
|
|
required
|
|
:rows="3"
|
|
:readonly="selectedSeriesId"
|
|
class="w-full"
|
|
/>
|
|
<p class="help-text">
|
|
{{
|
|
selectedSeriesId
|
|
? "From selected series"
|
|
: "Describe what the series covers and its goals"
|
|
}}
|
|
</p>
|
|
</div>
|
|
|
|
<div v-if="selectedSeriesId" class="note-box">
|
|
<strong>Note:</strong> This event will be added to the existing
|
|
"{{ eventForm.series.title }}" series.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Event Agenda -->
|
|
<div class="form-section">
|
|
<h2 class="section-heading">Event Agenda</h2>
|
|
|
|
<div class="agenda-items">
|
|
<div
|
|
v-for="(item, index) in eventForm.agenda"
|
|
:key="index"
|
|
class="agenda-row"
|
|
>
|
|
<UInput
|
|
v-model="eventForm.agenda[index]"
|
|
placeholder="Enter agenda item (e.g., 'Introduction and welcome - 10 mins')"
|
|
class="w-full"
|
|
/>
|
|
<button
|
|
type="button"
|
|
@click="removeAgendaItem(index)"
|
|
class="link-btn link-btn-danger"
|
|
>
|
|
<Icon name="heroicons:trash" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
@click="addAgendaItem"
|
|
class="btn add-agenda-btn"
|
|
>
|
|
+ Add Agenda Item
|
|
</button>
|
|
</div>
|
|
|
|
<p class="help-text">
|
|
Add agenda items to help attendees know what to expect during the
|
|
event
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Event Settings -->
|
|
<div class="form-section">
|
|
<h2 class="section-heading">Event Settings</h2>
|
|
|
|
<div class="form-grid">
|
|
<div class="check-group">
|
|
<label class="check-label">
|
|
<input v-model="eventForm.isOnline" type="checkbox" />
|
|
<div>
|
|
<strong>Online Event</strong>
|
|
<span class="help-text">
|
|
Event will be conducted virtually
|
|
</span>
|
|
</div>
|
|
</label>
|
|
|
|
<label class="check-label">
|
|
<input
|
|
v-model="eventForm.registrationRequired"
|
|
type="checkbox"
|
|
/>
|
|
<div>
|
|
<strong>Registration Required</strong>
|
|
<span class="help-text">
|
|
Attendees must register before attending
|
|
</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="check-group">
|
|
<label class="check-label">
|
|
<input v-model="eventForm.isVisible" type="checkbox" />
|
|
<div>
|
|
<strong>Visible on Public Calendar</strong>
|
|
<span class="help-text">
|
|
Event will appear on the public events page
|
|
</span>
|
|
</div>
|
|
</label>
|
|
|
|
<label class="check-label">
|
|
<input v-model="eventForm.isCancelled" type="checkbox" />
|
|
<div>
|
|
<strong>Event Cancelled</strong>
|
|
<span class="help-text"> Mark this event as cancelled </span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cancellation Message (conditional) -->
|
|
<div v-if="eventForm.isCancelled" class="form-section">
|
|
<div class="field">
|
|
<label>Cancellation Message</label>
|
|
<UTextarea
|
|
v-model="eventForm.cancellationMessage"
|
|
placeholder="Explain why the event was cancelled and any next steps..."
|
|
:rows="3"
|
|
color="error"
|
|
class="w-full"
|
|
/>
|
|
<p class="help-text">
|
|
This message will be displayed to users viewing the event page
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Form Actions -->
|
|
<div class="form-actions">
|
|
<NuxtLink to="/admin/events" class="btn"> Cancel </NuxtLink>
|
|
|
|
<div class="form-actions-right">
|
|
<button
|
|
v-if="!editingEvent"
|
|
type="button"
|
|
@click="saveAndCreateAnother"
|
|
:disabled="creating"
|
|
class="btn"
|
|
>
|
|
{{ creating ? "Saving..." : "Save & Create Another" }}
|
|
</button>
|
|
|
|
<button type="submit" :disabled="creating" class="btn btn-primary">
|
|
{{
|
|
creating
|
|
? "Saving..."
|
|
: editingEvent
|
|
? "Update Event"
|
|
: "Create Event"
|
|
}}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
definePageMeta({
|
|
layout: "admin",
|
|
middleware: "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(null);
|
|
const availableSeries = ref([]);
|
|
const availableTags = ref([]);
|
|
|
|
const tagOptions = computed(() =>
|
|
availableTags.value.map((t) => ({ label: t.label, value: t.slug }))
|
|
);
|
|
|
|
const eventForm = reactive({
|
|
title: "",
|
|
description: "",
|
|
content: "",
|
|
featureImage: null,
|
|
startDate: "",
|
|
endDate: "",
|
|
eventType: "community",
|
|
location: "",
|
|
isOnline: true,
|
|
isVisible: true,
|
|
isCancelled: false,
|
|
cancellationMessage: "",
|
|
targetCircles: [],
|
|
tags: [],
|
|
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 and tags
|
|
onMounted(async () => {
|
|
try {
|
|
const [seriesResponse, tagsResponse] = await Promise.all([
|
|
$fetch("/api/admin/series"),
|
|
$fetch("/api/tags"),
|
|
]);
|
|
availableSeries.value = seriesResponse;
|
|
availableTags.value = tagsResponse.tags || [];
|
|
} catch (error) {
|
|
console.error("Failed to load form data:", error);
|
|
}
|
|
});
|
|
|
|
// Handle series selection
|
|
const onSeriesSelect = () => {
|
|
console.log(
|
|
"onSeriesSelect called, selectedSeriesId:",
|
|
selectedSeriesId.value,
|
|
);
|
|
console.log("availableSeries:", availableSeries.value);
|
|
if (selectedSeriesId.value) {
|
|
const series = availableSeries.value.find(
|
|
(s) => s.id === selectedSeriesId.value,
|
|
);
|
|
console.log("Found series:", series);
|
|
if (series) {
|
|
eventForm.series.id = series.id;
|
|
eventForm.series.title = series.title;
|
|
eventForm.series.description = series.description;
|
|
console.log("Updated eventForm.series:", eventForm.series);
|
|
}
|
|
} 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 || [],
|
|
tags: event.tags || [],
|
|
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: [],
|
|
tags: [],
|
|
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>
|
|
|
|
<style scoped>
|
|
.create-form {
|
|
max-width: 800px;
|
|
}
|
|
|
|
.page-header {
|
|
padding: 28px 28px 20px;
|
|
border-bottom: 1px dashed var(--border);
|
|
}
|
|
|
|
.page-header h1 {
|
|
font-family: "Brygada 1918", serif;
|
|
font-size: 24px;
|
|
font-weight: 500;
|
|
color: var(--text-bright);
|
|
line-height: 1.2;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.page-header p {
|
|
font-size: 12px;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.back-link {
|
|
font-size: 11px;
|
|
color: var(--text-faint);
|
|
text-decoration: none;
|
|
margin-bottom: 8px;
|
|
display: inline-block;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
.back-link:hover {
|
|
color: var(--candle);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.form-body {
|
|
padding: 24px 28px;
|
|
}
|
|
|
|
.section-heading {
|
|
font-family: "Brygada 1918", serif;
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
color: var(--text-bright);
|
|
padding-bottom: 10px;
|
|
border-bottom: 1px dashed var(--border);
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.error-box {
|
|
padding: 16px 20px;
|
|
border: 1px dashed var(--ember);
|
|
margin-bottom: 20px;
|
|
font-size: 12px;
|
|
color: var(--ember);
|
|
}
|
|
|
|
.error-box h3 {
|
|
font-weight: 500;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.error-box ul {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.error-box li::before {
|
|
content: "\2022 ";
|
|
}
|
|
|
|
.success-box {
|
|
padding: 16px 20px;
|
|
border: 1px dashed var(--candle);
|
|
margin-bottom: 20px;
|
|
font-size: 12px;
|
|
color: var(--candle);
|
|
}
|
|
|
|
.help-text {
|
|
font-size: 11px;
|
|
color: var(--text-faint);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.field-error {
|
|
font-size: 11px;
|
|
color: var(--ember);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.required {
|
|
color: var(--ember);
|
|
}
|
|
|
|
.check-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.check-label {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: flex-start;
|
|
cursor: pointer;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.check-label input[type="checkbox"] {
|
|
width: auto;
|
|
flex-shrink: 0;
|
|
margin-top: 2px;
|
|
accent-color: var(--candle);
|
|
}
|
|
|
|
.check-label strong {
|
|
display: block;
|
|
font-size: 13px;
|
|
color: var(--text);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.check-label .help-text {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.form-section {
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.form-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 16px;
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding-top: 20px;
|
|
border-top: 1px dashed var(--border);
|
|
}
|
|
|
|
.form-actions-right {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.nested-section {
|
|
padding: 16px 20px;
|
|
border: 1px dashed var(--border);
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.note-box {
|
|
padding: 12px 16px;
|
|
border: 1px dashed var(--candle);
|
|
font-size: 12px;
|
|
color: var(--candle);
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.series-select-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.series-select-row .w-full {
|
|
flex: 1;
|
|
}
|
|
|
|
.agenda-items {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.agenda-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
|
|
.agenda-row .w-full {
|
|
flex: 1;
|
|
}
|
|
|
|
.link-btn {
|
|
background: none;
|
|
border: none;
|
|
color: var(--candle);
|
|
cursor: pointer;
|
|
font-family: 'Commit Mono', monospace;
|
|
font-size: 11px;
|
|
padding: 2px 6px;
|
|
}
|
|
|
|
.link-btn:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.link-btn-danger {
|
|
color: var(--ember);
|
|
}
|
|
|
|
.add-agenda-btn {
|
|
align-self: flex-start;
|
|
color: var(--candle);
|
|
border-color: var(--candle);
|
|
border-style: dashed;
|
|
}
|
|
|
|
.btn:disabled,
|
|
.btn-primary:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.page-header {
|
|
padding: 24px 20px 16px;
|
|
}
|
|
.form-body {
|
|
padding: 20px;
|
|
}
|
|
.form-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.series-select-row {
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
</style>
|