Enhance application structure: Add runtime configuration for environment variables, integrate new dependencies for Cloudinary and UI components, and refactor member management features including improved forms and member dashboard. Update styles and layout for better user experience.

This commit is contained in:
Jennie Robinson Faber 2025-08-27 16:49:51 +01:00
parent 6e7e27ac4e
commit e4a0a9ab0f
61 changed files with 7902 additions and 950 deletions

View file

@ -0,0 +1,361 @@
<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">
<h1 class="text-2xl font-bold text-gray-900">Event Management</h1>
<p class="text-gray-600">Create, manage, and monitor Ghost Guild events and workshops</p>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Search and Actions -->
<div class="mb-6 flex justify-between items-center">
<div class="flex gap-4 items-center">
<input v-model="searchQuery" placeholder="Search events..." class="border border-gray-300 rounded-lg px-4 py-2 w-80 focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
<select v-model="typeFilter" class="border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<option value="">All Types</option>
<option value="community">Community</option>
<option value="workshop">Workshop</option>
<option value="social">Social</option>
<option value="showcase">Showcase</option>
</select>
<select v-model="statusFilter" class="border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<option value="">All Status</option>
<option value="upcoming">Upcoming</option>
<option value="ongoing">Ongoing</option>
<option value="past">Past</option>
</select>
</div>
<button @click="showCreateModal = true" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
Create Event
</button>
</div>
<!-- Events Table -->
<div class="bg-white rounded-lg shadow overflow-hidden">
<div v-if="pending" class="p-8 text-center">
<div class="inline-flex items-center">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mr-3"></div>
Loading events...
</div>
</div>
<div v-else-if="error" class="p-8 text-center text-red-600">
Error loading events: {{ error }}
</div>
<table v-else class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Title</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Start Date</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Registration</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="event in filteredEvents" :key="event._id" class="hover:bg-gray-50">
<td class="px-6 py-4">
<div class="text-sm font-medium text-gray-900">{{ event.title }}</div>
<div class="text-sm text-gray-500">{{ event.description.substring(0, 100) }}...</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="getEventTypeClasses(event.eventType)" class="inline-flex px-2 py-1 text-xs font-semibold rounded-full">
{{ event.eventType }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
{{ formatDateTime(event.startDate) }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="getStatusClasses(event)" class="inline-flex px-2 py-1 text-xs font-semibold rounded-full">
{{ getEventStatus(event) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="event.registrationRequired ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800'" class="inline-flex px-2 py-1 text-xs font-semibold rounded-full">
{{ event.registrationRequired ? 'Required' : 'Open' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
<div class="flex gap-2">
<button @click="editEvent(event)" class="text-indigo-600 hover:text-indigo-900">Edit</button>
<button @click="duplicateEvent(event)" class="text-blue-600 hover:text-blue-900">Duplicate</button>
<button @click="deleteEvent(event)" class="text-red-600 hover:text-red-900">Delete</button>
</div>
</td>
</tr>
</tbody>
</table>
<div v-if="!pending && !error && filteredEvents.length === 0" class="p-8 text-center text-gray-500">
No events found matching your criteria
</div>
</div>
</div>
<!-- Create/Edit Event Modal -->
<div v-if="showCreateModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 overflow-y-auto">
<div class="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 my-8">
<div class="px-6 py-4 border-b">
<h3 class="text-lg font-semibold">
{{ editingEvent ? 'Edit Event' : 'Create New Event' }}
</h3>
</div>
<form @submit.prevent="saveEvent" class="p-6 space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Event Title</label>
<input v-model="eventForm.title" placeholder="Enter 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" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Event Type</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>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Location</label>
<input v-model="eventForm.location" placeholder="Event location" 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-1">Start Date & Time</label>
<input v-model="eventForm.startDate" type="datetime-local" required 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-1">End Date & Time</label>
<input v-model="eventForm.endDate" type="datetime-local" required 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-1">Max Attendees</label>
<input v-model="eventForm.maxAttendees" type="number" placeholder="Optional" 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-1">Registration Deadline</label>
<input v-model="eventForm.registrationDeadline" type="datetime-local" placeholder="Optional" 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>
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea v-model="eventForm.description" placeholder="Event description" required rows="3" 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>
<label class="block text-sm font-medium text-gray-700 mb-1">Additional Content</label>
<textarea v-model="eventForm.content" placeholder="Detailed event information, agenda, etc." 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"></textarea>
</div>
<div class="flex items-center gap-6">
<label class="flex items-center">
<input v-model="eventForm.isOnline" type="checkbox" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
<span class="ml-2 text-sm text-gray-700">Online Event</span>
</label>
<label class="flex items-center">
<input v-model="eventForm.registrationRequired" type="checkbox" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
<span class="ml-2 text-sm text-gray-700">Registration Required</span>
</label>
</div>
<div class="flex justify-end gap-3 pt-4">
<button type="button" @click="cancelEdit" class="px-4 py-2 text-gray-600 hover:text-gray-900">
Cancel
</button>
<button type="submit" :disabled="creating" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed">
{{ creating ? 'Saving...' : (editingEvent ? 'Update Event' : 'Create Event') }}
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup>
definePageMeta({
layout: 'admin'
})
const { data: events, pending, error, refresh } = await useFetch("/api/admin/events")
const searchQuery = ref('')
const typeFilter = ref('')
const statusFilter = ref('')
const showCreateModal = ref(false)
const creating = ref(false)
const editingEvent = ref(null)
const eventForm = reactive({
title: '',
description: '',
content: '',
startDate: '',
endDate: '',
eventType: 'community',
location: '',
isOnline: false,
maxAttendees: '',
registrationRequired: false,
registrationDeadline: ''
})
const filteredEvents = computed(() => {
if (!events.value) return []
return events.value.filter(event => {
const matchesSearch = !searchQuery.value ||
event.title.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
event.description.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchesType = !typeFilter.value || event.eventType === typeFilter.value
const eventStatus = getEventStatus(event)
const matchesStatus = !statusFilter.value || eventStatus.toLowerCase() === statusFilter.value
return matchesSearch && matchesType && matchesStatus
})
})
const getEventTypeClasses = (type) => {
const classes = {
community: 'bg-blue-100 text-blue-800',
workshop: 'bg-green-100 text-green-800',
social: 'bg-purple-100 text-purple-800',
showcase: 'bg-orange-100 text-orange-800'
}
return classes[type] || 'bg-gray-100 text-gray-800'
}
const getEventStatus = (event) => {
const now = new Date()
const startDate = new Date(event.startDate)
const endDate = new Date(event.endDate)
if (now < startDate) return 'Upcoming'
if (now >= startDate && now <= endDate) return 'Ongoing'
return 'Past'
}
const getStatusClasses = (event) => {
const status = getEventStatus(event)
const classes = {
'Upcoming': 'bg-blue-100 text-blue-800',
'Ongoing': 'bg-green-100 text-green-800',
'Past': 'bg-gray-100 text-gray-800'
}
return classes[status] || 'bg-gray-100 text-gray-800'
}
const formatDateTime = (dateString) => {
return new Date(dateString).toLocaleString()
}
const saveEvent = async () => {
creating.value = true
try {
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
})
}
cancelEdit()
await refresh()
alert('Event saved successfully!')
} catch (error) {
console.error('Failed to save event:', error)
alert('Failed to save event')
} finally {
creating.value = false
}
}
const editEvent = (event) => {
editingEvent.value = event
Object.assign(eventForm, {
title: event.title,
description: event.description,
content: event.content || '',
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,
maxAttendees: event.maxAttendees || '',
registrationRequired: event.registrationRequired,
registrationDeadline: event.registrationDeadline ? new Date(event.registrationDeadline).toISOString().slice(0, 16) : ''
})
showCreateModal.value = true
}
const duplicateEvent = (event) => {
editingEvent.value = null
Object.assign(eventForm, {
title: `${event.title} (Copy)`,
description: event.description,
content: event.content || '',
startDate: '',
endDate: '',
eventType: event.eventType,
location: event.location || '',
isOnline: event.isOnline,
maxAttendees: event.maxAttendees || '',
registrationRequired: event.registrationRequired,
registrationDeadline: ''
})
showCreateModal.value = true
}
const cancelEdit = () => {
showCreateModal.value = false
editingEvent.value = null
Object.assign(eventForm, {
title: '',
description: '',
content: '',
startDate: '',
endDate: '',
eventType: 'community',
location: '',
isOnline: false,
maxAttendees: '',
registrationRequired: false,
registrationDeadline: ''
})
}
const deleteEvent = async (event) => {
if (confirm(`Are you sure you want to delete "${event.title}"?`)) {
try {
await $fetch(`/api/admin/events/${event._id}`, {
method: 'DELETE'
})
await refresh()
alert('Event deleted successfully!')
} catch (error) {
console.error('Failed to delete event:', error)
alert('Failed to delete event')
}
}
}
</script>