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,238 @@
<template>
<div class="space-y-2">
<div class="relative">
<input
v-model="naturalInput"
type="text"
:placeholder="placeholder"
:class="[
'w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent',
inputClass,
{
'border-green-300 bg-green-50': isValidParse && naturalInput.trim(),
'border-red-300 bg-red-50': hasError && naturalInput.trim()
}
]"
@input="parseNaturalInput"
@blur="onBlur"
/>
<div v-if="naturalInput.trim()" class="absolute right-3 top-2.5">
<Icon
v-if="isValidParse"
name="heroicons:check-circle"
class="w-5 h-5 text-green-500"
/>
<Icon
v-else-if="hasError"
name="heroicons:exclamation-circle"
class="w-5 h-5 text-red-500"
/>
</div>
</div>
<div v-if="parsedDate && isValidParse" class="text-sm text-green-700 bg-green-50 px-3 py-2 rounded-lg border border-green-200">
<div class="flex items-center gap-2">
<Icon name="heroicons:calendar" class="w-4 h-4" />
<span>{{ formatParsedDate(parsedDate) }}</span>
</div>
</div>
<div v-if="hasError && naturalInput.trim()" class="text-sm text-red-700 bg-red-50 px-3 py-2 rounded-lg border border-red-200">
<div class="flex items-center gap-2">
<Icon name="heroicons:exclamation-triangle" class="w-4 h-4" />
<span>{{ errorMessage }}</span>
</div>
</div>
<!-- Fallback datetime-local input -->
<details class="text-sm">
<summary class="cursor-pointer text-gray-600 hover:text-gray-900">
Use traditional date picker
</summary>
<div class="mt-2">
<input
v-model="datetimeValue"
type="datetime-local"
:class="[
'w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent',
inputClass
]"
@change="onDatetimeChange"
/>
</div>
</details>
</div>
</template>
<script setup>
import * as chrono from 'chrono-node'
const props = defineProps({
modelValue: {
type: String,
default: ''
},
placeholder: {
type: String,
default: 'e.g., "tomorrow at 3pm", "next Friday at 9am", "in 2 hours"'
},
inputClass: {
type: String,
default: ''
},
required: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue'])
const naturalInput = ref('')
const parsedDate = ref(null)
const isValidParse = ref(false)
const hasError = ref(false)
const errorMessage = ref('')
const datetimeValue = ref('')
// Initialize with current value
onMounted(() => {
if (props.modelValue) {
const date = new Date(props.modelValue)
if (!isNaN(date.getTime())) {
parsedDate.value = date
datetimeValue.value = formatForDatetimeLocal(date)
isValidParse.value = true
}
}
})
// Watch for external changes to modelValue
watch(() => props.modelValue, (newValue) => {
if (newValue && newValue !== formatForDatetimeLocal(parsedDate.value)) {
const date = new Date(newValue)
if (!isNaN(date.getTime())) {
parsedDate.value = date
datetimeValue.value = formatForDatetimeLocal(date)
isValidParse.value = true
naturalInput.value = '' // Clear natural input when set externally
}
} else if (!newValue) {
reset()
}
})
const parseNaturalInput = () => {
const input = naturalInput.value.trim()
if (!input) {
reset()
return
}
try {
// Parse with chrono-node
const results = chrono.parse(input)
if (results.length > 0) {
const result = results[0]
const date = result.date()
// Validate the parsed date
if (date && !isNaN(date.getTime())) {
parsedDate.value = date
isValidParse.value = true
hasError.value = false
datetimeValue.value = formatForDatetimeLocal(date)
emit('update:modelValue', formatForDatetimeLocal(date))
} else {
setError('Could not parse this date format')
}
} else {
setError('Could not understand this date format. Try something like "tomorrow at 3pm" or "next Friday"')
}
} catch (error) {
setError('Error parsing date')
}
}
const onBlur = () => {
// If we have a valid parse but the input changed, try to parse again
if (naturalInput.value.trim() && !isValidParse.value) {
parseNaturalInput()
}
}
const onDatetimeChange = () => {
if (datetimeValue.value) {
const date = new Date(datetimeValue.value)
if (!isNaN(date.getTime())) {
parsedDate.value = date
isValidParse.value = true
hasError.value = false
naturalInput.value = '' // Clear natural input when using traditional picker
emit('update:modelValue', datetimeValue.value)
}
} else {
reset()
}
}
const reset = () => {
parsedDate.value = null
isValidParse.value = false
hasError.value = false
errorMessage.value = ''
emit('update:modelValue', '')
}
const setError = (message) => {
isValidParse.value = false
hasError.value = true
errorMessage.value = message
parsedDate.value = null
}
const formatForDatetimeLocal = (date) => {
if (!date) return ''
// Format as YYYY-MM-DDTHH:MM for datetime-local input
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day}T${hours}:${minutes}`
}
const formatParsedDate = (date) => {
if (!date) return ''
const now = new Date()
const isToday = date.toDateString() === now.toDateString()
const tomorrow = new Date(now)
tomorrow.setDate(tomorrow.getDate() + 1)
const isTomorrow = date.toDateString() === tomorrow.toDateString()
const timeStr = date.toLocaleString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
})
if (isToday) {
return `Today at ${timeStr}`
} else if (isTomorrow) {
return `Tomorrow at ${timeStr}`
} else {
return date.toLocaleString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true
})
}
}
</script>