Standardizes color values and styling using the new tokens: - Replaces hardcoded colors with semantic variables - Updates background/text/border classes for light/dark mode - Migrates inputs to UInput/USelect/UTextarea components - Removes redundant style declarations
359 lines
12 KiB
Vue
359 lines
12 KiB
Vue
<template>
|
|
<div>
|
|
<div class="bg-elevated border-b border-default">
|
|
<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-highlighted">Admin Dashboard</h1>
|
|
<p class="text-muted">
|
|
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-elevated rounded-lg shadow p-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm text-muted">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-elevated rounded-lg shadow p-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm text-muted">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-elevated rounded-lg shadow p-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm text-muted">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-elevated rounded-lg shadow p-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm text-muted">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 gap-6 mb-8">
|
|
<div class="bg-elevated 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 text-highlighted">
|
|
Add New Member
|
|
</h3>
|
|
<p class="text-muted 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-elevated 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 text-highlighted">
|
|
Create Event
|
|
</h3>
|
|
<p class="text-muted 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>
|
|
|
|
<!-- Recent Activity -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<div class="bg-elevated rounded-lg shadow">
|
|
<div class="px-6 py-4 border-b border-default">
|
|
<div class="flex justify-between items-center">
|
|
<h3 class="text-lg font-semibold text-highlighted">
|
|
Recent Members
|
|
</h3>
|
|
<button
|
|
@click="navigateTo('/admin/members')"
|
|
class="text-sm text-primary hover:text-primary"
|
|
>
|
|
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-default"
|
|
>
|
|
<div>
|
|
<p class="font-medium text-highlighted">
|
|
{{ member.name }}
|
|
</p>
|
|
<p class="text-sm text-muted">
|
|
{{ 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-dimmed">
|
|
{{ formatDate(member.createdAt) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="text-center py-6 text-dimmed">
|
|
No recent members
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-elevated rounded-lg shadow">
|
|
<div class="px-6 py-4 border-b border-default">
|
|
<div class="flex justify-between items-center">
|
|
<h3 class="text-lg font-semibold text-highlighted">
|
|
Upcoming Events
|
|
</h3>
|
|
<button
|
|
@click="navigateTo('/admin/events')"
|
|
class="text-sm text-primary hover:text-primary"
|
|
>
|
|
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-default"
|
|
>
|
|
<div>
|
|
<p class="font-medium text-highlighted">
|
|
{{ event.title }}
|
|
</p>
|
|
<p class="text-sm text-muted">
|
|
{{ 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-dimmed">
|
|
{{ event.location || "Online" }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="text-center py-6 text-dimmed">
|
|
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-accented text-default";
|
|
};
|
|
|
|
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-accented text-default";
|
|
};
|
|
|
|
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>
|