Add series management and ticketing features: Introduce series event functionality in event creation, enhance event display with series information, and implement ticketing options for public events. Update layouts and improve form handling for better user experience.

This commit is contained in:
Jennie Robinson Faber 2025-08-27 20:40:54 +01:00
parent c3a29fa47c
commit a88aa62198
24 changed files with 2897 additions and 44 deletions

View file

@ -0,0 +1,268 @@
<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/series-management" 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">Create New Series</h1>
</div>
<p class="text-gray-600">Create a new event series to group related events together</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">Series created successfully!</h3>
</div>
</div>
</div>
<form @submit.prevent="createSeries">
<!-- Series Information -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Series Information</h2>
<div class="grid grid-cols-1 gap-6">
<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="seriesForm.title"
type="text"
placeholder="e.g., Cooperative Game Development Fundamentals"
required
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
:class="{ 'border-red-300 focus:ring-red-500': fieldErrors.title }"
@input="generateSlugFromTitle"
/>
<p v-if="fieldErrors.title" class="mt-1 text-sm text-red-600">{{ fieldErrors.title }}</p>
</div>
<div v-if="generatedSlug">
<label class="block text-sm font-medium text-gray-700 mb-2">Generated Series ID</label>
<div class="w-full bg-gray-100 border border-gray-300 rounded-lg px-3 py-2 text-gray-700 font-mono text-sm">
{{ generatedSlug }}
</div>
<p class="mt-1 text-sm text-gray-500">
This unique identifier will be automatically generated from your title
</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="seriesForm.description"
placeholder="Describe what the series covers and its goals"
required
rows="4"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-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>
</div>
<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">Series Type</label>
<select
v-model="seriesForm.type"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<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="seriesForm.totalEvents"
type="number"
min="1"
placeholder="e.g., 4"
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-sm text-gray-500 mt-1">How many events will be in this series? (optional)</p>
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="flex justify-between items-center pt-6 border-t border-gray-200">
<NuxtLink
to="/admin/series-management"
class="px-4 py-2 text-gray-600 hover:text-gray-900 font-medium"
>
Cancel
</NuxtLink>
<div class="flex gap-3">
<button
type="button"
@click="createAndAddEvent"
:disabled="creating"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
>
{{ creating ? 'Creating...' : 'Create & Add Event' }}
</button>
<button
type="submit"
:disabled="creating"
class="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
>
{{ creating ? 'Creating...' : 'Create Series' }}
</button>
</div>
</div>
</form>
</div>
</div>
</template>
<script setup>
definePageMeta({
layout: 'admin'
})
const router = useRouter()
const creating = ref(false)
const showSuccessMessage = ref(false)
const formErrors = ref([])
const fieldErrors = ref({})
const generatedSlug = ref('')
const seriesForm = reactive({
title: '',
description: '',
type: 'workshop_series',
totalEvents: null
})
// Generate slug from title
const generateSlug = (text) => {
return text
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters except spaces and dashes
.replace(/\s+/g, '-') // Replace spaces with dashes
.replace(/-+/g, '-') // Replace multiple dashes with single dash
.replace(/^-+|-+$/g, '') // Remove leading/trailing dashes
}
const generateSlugFromTitle = () => {
if (seriesForm.title) {
generatedSlug.value = generateSlug(seriesForm.title)
} else {
generatedSlug.value = ''
}
}
const validateForm = () => {
formErrors.value = []
fieldErrors.value = {}
if (!seriesForm.title.trim()) {
formErrors.value.push('Series title is required')
fieldErrors.value.title = 'Please enter a series title'
}
if (!seriesForm.description.trim()) {
formErrors.value.push('Series description is required')
fieldErrors.value.description = 'Please provide a description for the series'
}
if (!generatedSlug.value) {
formErrors.value.push('Series title must generate a valid ID')
fieldErrors.value.title = 'Please enter a title that can generate a valid series ID'
}
return formErrors.value.length === 0
}
const createSeries = async (redirectAfter = true) => {
if (!validateForm()) {
window.scrollTo({ top: 0, behavior: 'smooth' })
return false
}
creating.value = true
try {
const response = await $fetch('/api/admin/series', {
method: 'POST',
body: {
...seriesForm,
id: generatedSlug.value
}
})
showSuccessMessage.value = true
setTimeout(() => { showSuccessMessage.value = false }, 5000)
if (redirectAfter) {
setTimeout(() => {
router.push('/admin/series-management')
}, 1500)
}
return response.data
} catch (error) {
console.error('Failed to create series:', error)
formErrors.value = [`Failed to create series: ${error.data?.statusMessage || error.message}`]
window.scrollTo({ top: 0, behavior: 'smooth' })
return false
} finally {
creating.value = false
}
}
const createAndAddEvent = async () => {
const series = await createSeries(false)
if (series) {
// Navigate to event creation with series pre-filled
const seriesData = {
series: {
isSeriesEvent: true,
id: generatedSlug.value,
title: seriesForm.title,
description: seriesForm.description,
type: seriesForm.type,
position: 1,
totalEvents: seriesForm.totalEvents
}
}
sessionStorage.setItem('seriesEventData', JSON.stringify(seriesData))
router.push('/admin/events/create?series=true')
}
}
</script>