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:
parent
6e7e27ac4e
commit
e4a0a9ab0f
61 changed files with 7902 additions and 950 deletions
250
app/pages/admin/index.vue
Normal file
250
app/pages/admin/index.vue
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
<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">Admin Dashboard</h1>
|
||||
<p class="text-gray-600">Manage Ghost Guild members, events, and community operations</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Quick Stats -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Total Members</p>
|
||||
<p class="text-2xl font-bold text-blue-600">
|
||||
{{ stats.totalMembers || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-.5a4 4 0 110 5.292M4 19.5a4 4 0 010-5.292"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Active Events</p>
|
||||
<p class="text-2xl font-bold text-green-600">
|
||||
{{ stats.activeEvents || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Monthly Revenue</p>
|
||||
<p class="text-2xl font-bold text-purple-600">
|
||||
${{ stats.monthlyRevenue || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Pending Slack Invites</p>
|
||||
<p class="text-2xl font-bold text-orange-600">
|
||||
{{ stats.pendingSlackInvites || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-orange-100 rounded-xl flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-blue-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2">Add New Member</h3>
|
||||
<p class="text-gray-600 text-sm mb-4">
|
||||
Add a new member to the Ghost Guild community
|
||||
</p>
|
||||
<button @click="navigateTo('/admin/members')" class="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
||||
Manage Members
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-green-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2">Create Event</h3>
|
||||
<p class="text-gray-600 text-sm mb-4">
|
||||
Schedule a new community event or workshop
|
||||
</p>
|
||||
<button @click="navigateTo('/admin/events')" class="w-full bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-700 focus:ring-2 focus:ring-green-500 focus:ring-offset-2">
|
||||
Manage Events
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-purple-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2">View Analytics</h3>
|
||||
<p class="text-gray-600 text-sm mb-4">
|
||||
Review member engagement and growth metrics
|
||||
</p>
|
||||
<button disabled class="w-full bg-gray-300 text-gray-500 py-2 px-4 rounded-lg cursor-not-allowed">
|
||||
Coming Soon
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold">Recent Members</h3>
|
||||
<button @click="navigateTo('/admin/members')" class="text-sm text-blue-600 hover:text-blue-900">
|
||||
View All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div v-if="pending" class="text-center py-4">
|
||||
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mx-auto"></div>
|
||||
</div>
|
||||
<div v-else-if="recentMembers.length" class="space-y-3">
|
||||
<div v-for="member in recentMembers" :key="member._id" class="flex items-center justify-between p-3 rounded-lg border border-gray-200">
|
||||
<div>
|
||||
<p class="font-medium">{{ member.name }}</p>
|
||||
<p class="text-sm text-gray-600">{{ member.email }}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span :class="getCircleBadgeClasses(member.circle)" class="inline-flex px-2 py-1 text-xs font-semibold rounded-full mb-1">
|
||||
{{ member.circle }}
|
||||
</span>
|
||||
<p class="text-xs text-gray-500">{{ formatDate(member.createdAt) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-6 text-gray-500">
|
||||
No recent members
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold">Upcoming Events</h3>
|
||||
<button @click="navigateTo('/admin/events')" class="text-sm text-blue-600 hover:text-blue-900">
|
||||
View All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div v-if="pending" class="text-center py-4">
|
||||
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mx-auto"></div>
|
||||
</div>
|
||||
<div v-else-if="upcomingEvents.length" class="space-y-3">
|
||||
<div v-for="event in upcomingEvents" :key="event._id" class="flex items-center justify-between p-3 rounded-lg border border-gray-200">
|
||||
<div>
|
||||
<p class="font-medium">{{ event.title }}</p>
|
||||
<p class="text-sm text-gray-600">{{ formatDateTime(event.startDate) }}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span :class="getEventTypeBadgeClasses(event.eventType)" class="inline-flex px-2 py-1 text-xs font-semibold rounded-full mb-1">
|
||||
{{ event.eventType }}
|
||||
</span>
|
||||
<p class="text-xs text-gray-500">{{ event.location || 'Online' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-6 text-gray-500">
|
||||
No upcoming events
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
})
|
||||
|
||||
const { data: dashboardData, pending } = await useFetch('/api/admin/dashboard')
|
||||
|
||||
const stats = computed(() => dashboardData.value?.stats || {})
|
||||
const recentMembers = computed(() => dashboardData.value?.recentMembers || [])
|
||||
const upcomingEvents = computed(() => dashboardData.value?.upcomingEvents || [])
|
||||
|
||||
const getCircleBadgeClasses = (circle) => {
|
||||
const classes = {
|
||||
community: 'bg-blue-100 text-blue-800',
|
||||
founder: 'bg-purple-100 text-purple-800',
|
||||
practitioner: 'bg-green-100 text-green-800'
|
||||
}
|
||||
return classes[circle] || 'bg-gray-100 text-gray-800'
|
||||
}
|
||||
|
||||
const getEventTypeBadgeClasses = (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 formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString()
|
||||
}
|
||||
|
||||
const formatDateTime = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue