feat: reskin admin pages to zine design system
Migrate the entire admin section from the dark guild-* Tailwind theme to the zine design system (dashed borders, CSS custom properties, Brygada 1918 + Commit Mono, cream/dark mode palette). - Replace admin top-nav layout with sidebar matching default layout - Reskin dashboard, members, events, series management pages - Reskin events/create and series/create form pages - Add dev-only test login endpoint (GET /api/dev/test-login) - Redirect duplicate admin/dashboard.vue to /admin - Update CLAUDE.md design system docs
This commit is contained in:
parent
f16f9ada64
commit
fcd6f4cdf4
23 changed files with 3845 additions and 3827 deletions
|
|
@ -1,359 +1,13 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="bg-guild-900 border-b border-guild-700">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="py-6">
|
||||
<h1 class="text-display font-bold text-guild-100">Admin Dashboard</h1>
|
||||
<p class="text-guild-400">
|
||||
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-guild-900 rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">Total Members</p>
|
||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">
|
||||
{{ stats.totalMembers || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
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-guild-900 rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">Active Events</p>
|
||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">
|
||||
{{ stats.activeEvents || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
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-guild-900 rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">Monthly Revenue</p>
|
||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">
|
||||
${{ stats.monthlyRevenue || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
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-guild-900 rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">Pending Slack Invites</p>
|
||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">
|
||||
{{ stats.pendingSlackInvites || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
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-guild-900 rounded-lg shadow p-6">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="w-16 h-16 bg-candlelight-900/20 rounded-2xl flex items-center justify-center mx-auto mb-4"
|
||||
>
|
||||
<svg
|
||||
class="w-8 h-8 text-candlelight-400"
|
||||
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-display-sm font-semibold text-guild-100 mb-2">
|
||||
Add New Member
|
||||
</h3>
|
||||
<p class="text-guild-400 text-sm mb-4">
|
||||
Add a new member to the Ghost Guild community
|
||||
</p>
|
||||
<button
|
||||
@click="navigateTo('/admin/members-working')"
|
||||
class="w-full bg-candlelight-600 text-white py-2 px-4 rounded-lg hover:bg-candlelight-700 focus:ring-2 focus:ring-candlelight-500 focus:ring-offset-2"
|
||||
>
|
||||
Manage Members
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="w-16 h-16 bg-candlelight-900/20 rounded-2xl flex items-center justify-center mx-auto mb-4"
|
||||
>
|
||||
<svg
|
||||
class="w-8 h-8 text-candlelight-400"
|
||||
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-display-sm font-semibold text-guild-100 mb-2">
|
||||
Create Event
|
||||
</h3>
|
||||
<p class="text-guild-400 text-sm mb-4">
|
||||
Schedule a new community event or workshop
|
||||
</p>
|
||||
<button
|
||||
@click="navigateTo('/admin/events-working')"
|
||||
class="w-full bg-candlelight-600 text-white py-2 px-4 rounded-lg hover:bg-candlelight-700 focus:ring-2 focus:ring-candlelight-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-guild-900 rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-guild-700">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-display-sm font-semibold text-guild-100">
|
||||
Recent Members
|
||||
</h3>
|
||||
<button
|
||||
@click="navigateTo('/admin/members-working')"
|
||||
class="text-sm text-candlelight-400 hover:text-candlelight-300"
|
||||
>
|
||||
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-candlelight-500 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-guild-700"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium text-guild-100">
|
||||
{{ member.name }}
|
||||
</p>
|
||||
<p class="text-sm text-guild-400">
|
||||
{{ 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-guild-500 text-ui-mono">
|
||||
{{ formatDate(member.createdAt) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-6 text-guild-500">
|
||||
No recent members
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-guild-900 rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-guild-700">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-display-sm font-semibold text-guild-100">
|
||||
Upcoming Events
|
||||
</h3>
|
||||
<button
|
||||
@click="navigateTo('/admin/events-working')"
|
||||
class="text-sm text-candlelight-400 hover:text-candlelight-300"
|
||||
>
|
||||
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-candlelight-500 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-guild-700"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium text-guild-100">
|
||||
{{ event.title }}
|
||||
</p>
|
||||
<p class="text-sm text-guild-400">
|
||||
{{ 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-guild-500">
|
||||
{{ event.location || "Online" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-6 text-guild-500">
|
||||
No upcoming events
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
layout: 'admin',
|
||||
middleware: '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-candlelight-900/20 text-candlelight-400",
|
||||
founder: "bg-earth-900/20 text-earth-400",
|
||||
practitioner: "bg-candlelight-900/20 text-candlelight-400",
|
||||
};
|
||||
return classes[circle] || "bg-guild-800 text-guild-300";
|
||||
};
|
||||
|
||||
const getEventTypeBadgeClasses = (type) => {
|
||||
const classes = {
|
||||
community: "bg-candlelight-900/20 text-candlelight-400",
|
||||
workshop: "bg-candlelight-900/20 text-candlelight-400",
|
||||
social: "bg-earth-900/20 text-earth-400",
|
||||
showcase: "bg-ember-900/20 text-ember-400",
|
||||
};
|
||||
return classes[type] || "bg-guild-800 text-guild-300";
|
||||
};
|
||||
|
||||
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",
|
||||
});
|
||||
};
|
||||
// Redirect to the main admin page
|
||||
await navigateTo('/admin', { replace: true })
|
||||
</script>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,312 +1,98 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="bg-guild-900 border-b border-guild-700">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="py-6">
|
||||
<h1 class="text-display font-bold text-guild-100">Admin Dashboard</h1>
|
||||
<p class="text-guild-400">
|
||||
Manage Ghost Guild members, events, and community operations
|
||||
</p>
|
||||
<div class="admin-dash">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h1>Admin Dashboard</h1>
|
||||
<p>Members, events, and community operations</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats + Quick Actions row -->
|
||||
<div class="content-row">
|
||||
<div class="content-block">
|
||||
<div class="section-label">Overview</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-key">Total Members</span>
|
||||
<span class="stat-val">{{ stats.totalMembers || 0 }}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-key">Active Events</span>
|
||||
<span class="stat-val">{{ stats.activeEvents || 0 }}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-key">Monthly Revenue</span>
|
||||
<span class="stat-val">${{ stats.monthlyRevenue || 0 }}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-key">Pending Slack Invites</span>
|
||||
<span class="stat-val">{{ stats.pendingSlackInvites || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-block">
|
||||
<div class="section-label">Quick Actions</div>
|
||||
<NuxtLink to="/admin/members" class="action-link">
|
||||
Manage Members<span class="arrow">→</span>
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/admin/events" class="action-link">
|
||||
Manage Events<span class="arrow">→</span>
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/admin/events/create" class="action-link">
|
||||
Create Event<span class="arrow">→</span>
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/admin/series/create" class="action-link">
|
||||
Create Series<span class="arrow">→</span>
|
||||
</NuxtLink>
|
||||
</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-guild-900 rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">Total Members</p>
|
||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">
|
||||
{{ stats.totalMembers || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
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>
|
||||
<!-- Recent Activity row -->
|
||||
<div class="content-row">
|
||||
<div class="content-block">
|
||||
<div class="section-label">Recent Members</div>
|
||||
|
||||
<div v-if="pending" class="loading-inline">
|
||||
<div class="spinner spinner-sm" />
|
||||
</div>
|
||||
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div v-else-if="recentMembers.length" class="item-list">
|
||||
<div v-for="member in recentMembers" :key="member._id" class="item-row">
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">Active Events</p>
|
||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">
|
||||
{{ stats.activeEvents || 0 }}
|
||||
</p>
|
||||
<span class="item-name">{{ member.name }}</span>
|
||||
<span class="item-sub">{{ member.email }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
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 class="item-meta">
|
||||
<span class="badge" :class="member.circle">{{ member.circle }}</span>
|
||||
<span class="item-date">{{ formatDate(member.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">No recent members</div>
|
||||
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">Monthly Revenue</p>
|
||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">
|
||||
${{ stats.monthlyRevenue || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
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-guild-900 rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">Pending Slack Invites</p>
|
||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">
|
||||
{{ stats.pendingSlackInvites || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
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>
|
||||
<NuxtLink to="/admin/members" class="section-link">View all members →</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="w-16 h-16 bg-candlelight-900/20 rounded-2xl flex items-center justify-center mx-auto mb-4"
|
||||
>
|
||||
<svg
|
||||
class="w-8 h-8 text-candlelight-400"
|
||||
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-display-sm font-semibold mb-2 text-guild-100">
|
||||
Add New Member
|
||||
</h3>
|
||||
<p class="text-guild-400 text-sm mb-4">
|
||||
Add a new member to the Ghost Guild community
|
||||
</p>
|
||||
<button
|
||||
@click="navigateTo('/admin/members')"
|
||||
class="w-full bg-candlelight-600 text-white py-2 px-4 rounded-lg hover:bg-candlelight-700 focus:ring-2 focus:ring-candlelight-500 focus:ring-offset-2"
|
||||
>
|
||||
Manage Members
|
||||
</button>
|
||||
</div>
|
||||
<div class="content-block">
|
||||
<div class="section-label">Upcoming Events</div>
|
||||
|
||||
<div v-if="pending" class="loading-inline">
|
||||
<div class="spinner spinner-sm" />
|
||||
</div>
|
||||
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="w-16 h-16 bg-candlelight-900/20 rounded-2xl flex items-center justify-center mx-auto mb-4"
|
||||
>
|
||||
<svg
|
||||
class="w-8 h-8 text-candlelight-400"
|
||||
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 v-else-if="upcomingEvents.length" class="item-list">
|
||||
<div v-for="event in upcomingEvents" :key="event._id" class="item-row">
|
||||
<div>
|
||||
<span class="item-name">{{ event.title }}</span>
|
||||
<span class="item-sub">{{ formatDateTime(event.startDate) }}</span>
|
||||
</div>
|
||||
<h3 class="text-display-sm font-semibold mb-2 text-guild-100">
|
||||
Create Event
|
||||
</h3>
|
||||
<p class="text-guild-400 text-sm mb-4">
|
||||
Schedule a new community event or workshop
|
||||
</p>
|
||||
<button
|
||||
@click="navigateTo('/admin/events')"
|
||||
class="w-full bg-candlelight-600 text-white py-2 px-4 rounded-lg hover:bg-candlelight-700 focus:ring-2 focus:ring-candlelight-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-guild-900 rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-guild-700">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-display-sm font-semibold text-guild-100">
|
||||
Recent Members
|
||||
</h3>
|
||||
<button
|
||||
@click="navigateTo('/admin/members')"
|
||||
class="text-sm text-candlelight-400 hover:text-candlelight-300"
|
||||
>
|
||||
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-candlelight-500 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-guild-700"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium text-guild-100">
|
||||
{{ member.name }}
|
||||
</p>
|
||||
<p class="text-sm text-guild-400">
|
||||
{{ 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-guild-500 text-ui-mono">
|
||||
{{ formatDate(member.createdAt) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-6 text-guild-500">
|
||||
No recent members
|
||||
<div class="item-meta">
|
||||
<span class="badge" :class="event.eventType">{{ event.eventType }}</span>
|
||||
<span class="item-date">{{ event.location || 'Online' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">No upcoming events</div>
|
||||
|
||||
<div class="bg-guild-900 rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-guild-700">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-display-sm font-semibold text-guild-100">
|
||||
Upcoming Events
|
||||
</h3>
|
||||
<button
|
||||
@click="navigateTo('/admin/events')"
|
||||
class="text-sm text-candlelight-400 hover:text-candlelight-300"
|
||||
>
|
||||
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-candlelight-500 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-guild-700"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium text-guild-100">
|
||||
{{ event.title }}
|
||||
</p>
|
||||
<p class="text-sm text-guild-400">
|
||||
{{ 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-guild-500">
|
||||
{{ event.location || "Online" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-6 text-guild-500">
|
||||
No upcoming events
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NuxtLink to="/admin/events" class="section-link">View all events →</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -314,46 +100,233 @@
|
|||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
layout: 'admin',
|
||||
middleware: 'admin',
|
||||
})
|
||||
|
||||
const { data: dashboardData, pending } = await useFetch("/api/admin/dashboard");
|
||||
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-candlelight-900/20 text-candlelight-400",
|
||||
founder: "bg-earth-900/20 text-earth-400",
|
||||
practitioner: "bg-candlelight-900/20 text-candlelight-400",
|
||||
};
|
||||
return classes[circle] || "bg-guild-800 text-guild-300";
|
||||
};
|
||||
|
||||
const getEventTypeBadgeClasses = (type) => {
|
||||
const classes = {
|
||||
community: "bg-candlelight-900/20 text-candlelight-400",
|
||||
workshop: "bg-candlelight-900/20 text-candlelight-400",
|
||||
social: "bg-earth-900/20 text-earth-400",
|
||||
showcase: "bg-ember-900/20 text-ember-400",
|
||||
};
|
||||
return classes[type] || "bg-guild-800 text-guild-300";
|
||||
};
|
||||
const stats = computed(() => dashboardData.value?.stats || {})
|
||||
const recentMembers = computed(() => dashboardData.value?.recentMembers || [])
|
||||
const upcomingEvents = computed(() => dashboardData.value?.upcomingEvents || [])
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString();
|
||||
};
|
||||
return new Date(dateString).toLocaleDateString()
|
||||
}
|
||||
|
||||
const formatDateTime = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString(undefined, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-dash {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ---- PAGE HEADER ---- */
|
||||
.page-header {
|
||||
padding: 28px 28px 20px;
|
||||
border-bottom: 1px dashed var(--border);
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-family: 'Brygada 1918', serif;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: var(--text-bright);
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* ---- CONTENT GRID ---- */
|
||||
.content-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
border-bottom: 1px dashed var(--border);
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding: 24px 28px;
|
||||
border-right: 1px dashed var(--border);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.content-block:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* ---- STATS ---- */
|
||||
.stat-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px dashed var(--border);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stat-row:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.stat-key {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.stat-val {
|
||||
color: var(--text-bright);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---- QUICK ACTIONS ---- */
|
||||
.action-link {
|
||||
border: 1px dashed var(--border);
|
||||
padding: 14px 20px;
|
||||
margin-bottom: 8px;
|
||||
transition: border-color 0.2s;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.action-link:hover {
|
||||
border-color: var(--candle-faint);
|
||||
color: var(--candle);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.action-link .arrow {
|
||||
color: var(--text-faint);
|
||||
margin-left: 8px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.action-link:hover .arrow {
|
||||
color: var(--candle-faint);
|
||||
}
|
||||
|
||||
/* ---- ITEM LIST (members / events) ---- */
|
||||
.item-list {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.item-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px dashed var(--border);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.item-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
display: block;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.item-sub {
|
||||
display: block;
|
||||
color: var(--text-faint);
|
||||
font-size: 11px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.item-meta {
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-date {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--text-faint);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ---- SECTION LINK ---- */
|
||||
.section-link {
|
||||
display: inline-block;
|
||||
margin-top: 16px;
|
||||
font-size: 12px;
|
||||
color: var(--candle);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.section-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ---- STATES ---- */
|
||||
.loading-inline {
|
||||
padding: 24px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px dashed var(--candle);
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.spinner-sm {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 24px 0;
|
||||
color: var(--text-faint);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ---- RESPONSIVE ---- */
|
||||
@media (max-width: 768px) {
|
||||
.content-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
border-right: none;
|
||||
border-bottom: 1px dashed var(--border);
|
||||
}
|
||||
|
||||
.content-block:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 24px 20px 16px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,143 +1,123 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="bg-guild-900 border-b border-guild-700">
|
||||
<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-guild-500 hover:text-guild-100">
|
||||
<Icon name="heroicons:arrow-left" class="w-5 h-5" />
|
||||
</NuxtLink>
|
||||
<h1 class="text-display font-bold text-guild-100">Create New Series</h1>
|
||||
</div>
|
||||
<p class="text-guild-400">Create a new event series to group related events together</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="create-form">
|
||||
<div class="page-header">
|
||||
<NuxtLink to="/admin/series-management" class="back-link">← Series</NuxtLink>
|
||||
<h1>Create New Series</h1>
|
||||
<p>Create a new event series to group related events together</p>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
<div class="form-body">
|
||||
<!-- Error Summary -->
|
||||
<div v-if="formErrors.length > 0" class="mb-6 p-4 bg-ember-900/20 border border-ember-800 rounded-lg">
|
||||
<div class="flex">
|
||||
<Icon name="heroicons:exclamation-circle" class="w-5 h-5 text-ember-400 mr-3 mt-0.5" />
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-ember-400 mb-2">Please fix the following errors:</h3>
|
||||
<ul class="text-sm text-ember-400 space-y-1">
|
||||
<li v-for="error in formErrors" :key="error">• {{ error }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="formErrors.length > 0" class="error-box">
|
||||
<Icon name="heroicons:exclamation-circle" class="box-icon" />
|
||||
<div>
|
||||
<strong>Please fix the following errors:</strong>
|
||||
<ul>
|
||||
<li v-for="error in formErrors" :key="error">{{ error }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div v-if="showSuccessMessage" class="mb-6 p-4 bg-candlelight-900/20 border border-candlelight-800 rounded-lg">
|
||||
<div class="flex">
|
||||
<Icon name="heroicons:check-circle" class="w-5 h-5 text-candlelight-400 mr-3 mt-0.5" />
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-candlelight-400">Series created successfully!</h3>
|
||||
</div>
|
||||
<div v-if="showSuccessMessage" class="success-box">
|
||||
<Icon name="heroicons:check-circle" class="box-icon" />
|
||||
<div>
|
||||
<strong>Series created successfully!</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="createSeries">
|
||||
<!-- Series Information -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-guild-100 mb-4">Series Information</h2>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">
|
||||
Series Title <span class="text-ember-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="seriesForm.title"
|
||||
type="text"
|
||||
placeholder="e.g., Cooperative Game Development Fundamentals"
|
||||
required
|
||||
class="w-full bg-guild-800 border border-guild-700 rounded-lg px-3 py-2 text-guild-100 placeholder-guild-500 focus:ring-2 focus:ring-earth-500 focus:border-transparent"
|
||||
:class="{ 'border-ember-700 focus:ring-ember-500': fieldErrors.title }"
|
||||
@input="generateSlugFromTitle"
|
||||
/>
|
||||
<p v-if="fieldErrors.title" class="mt-1 text-sm text-ember-400">{{ fieldErrors.title }}</p>
|
||||
<div class="form-section">
|
||||
<h2 class="section-heading">Series Information</h2>
|
||||
|
||||
<div class="field">
|
||||
<label>
|
||||
Series Title <span class="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="seriesForm.title"
|
||||
type="text"
|
||||
placeholder="e.g., Cooperative Game Development Fundamentals"
|
||||
required
|
||||
:class="{ 'has-error': fieldErrors.title }"
|
||||
@input="generateSlugFromTitle"
|
||||
/>
|
||||
<p v-if="fieldErrors.title" class="field-error">{{ fieldErrors.title }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="generatedSlug" class="field">
|
||||
<label>Generated Series ID</label>
|
||||
<div class="slug-display">
|
||||
{{ generatedSlug }}
|
||||
</div>
|
||||
<p class="help-text">
|
||||
This unique identifier will be automatically generated from your title
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>
|
||||
Series Description <span class="required">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
v-model="seriesForm.description"
|
||||
placeholder="Describe what the series covers and its goals"
|
||||
required
|
||||
rows="4"
|
||||
:class="{ 'has-error': fieldErrors.description }"
|
||||
></textarea>
|
||||
<p v-if="fieldErrors.description" class="field-error">{{ fieldErrors.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label>Series Type</label>
|
||||
<select v-model="seriesForm.type">
|
||||
<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 v-if="generatedSlug">
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">Generated Series ID</label>
|
||||
<div class="w-full bg-guild-800 border border-guild-700 rounded-lg px-3 py-2 text-guild-100 font-mono text-sm">
|
||||
{{ generatedSlug }}
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-guild-500">
|
||||
This unique identifier will be automatically generated from your title
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">
|
||||
Series Description <span class="text-ember-400">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
v-model="seriesForm.description"
|
||||
placeholder="Describe what the series covers and its goals"
|
||||
required
|
||||
rows="4"
|
||||
class="w-full bg-guild-800 border border-guild-700 rounded-lg px-3 py-2 text-guild-100 placeholder-guild-500 focus:ring-2 focus:ring-earth-500 focus:border-transparent"
|
||||
:class="{ 'border-ember-700 focus:ring-ember-500': fieldErrors.description }"
|
||||
></textarea>
|
||||
<p v-if="fieldErrors.description" class="mt-1 text-sm text-ember-400">{{ 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-guild-100 mb-2">Series Type</label>
|
||||
<select
|
||||
v-model="seriesForm.type"
|
||||
class="w-full bg-guild-800 border border-guild-700 rounded-lg px-3 py-2 text-guild-100 focus:ring-2 focus:ring-earth-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-guild-100 mb-2">Total Events Planned</label>
|
||||
<input
|
||||
v-model.number="seriesForm.totalEvents"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="e.g., 4"
|
||||
class="w-full bg-guild-800 border border-guild-700 rounded-lg px-3 py-2 text-guild-100 placeholder-guild-500 focus:ring-2 focus:ring-earth-500 focus:border-transparent"
|
||||
/>
|
||||
<p class="text-sm text-guild-500 mt-1">How many events will be in this series? (optional)</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Total Events Planned</label>
|
||||
<input
|
||||
v-model.number="seriesForm.totalEvents"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="e.g., 4"
|
||||
/>
|
||||
<p class="help-text">How many events will be in this series? (optional)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="flex justify-between items-center pt-6 border-t border-guild-700">
|
||||
<NuxtLink
|
||||
to="/admin/series-management"
|
||||
class="px-4 py-2 text-guild-400 hover:text-guild-100 font-medium"
|
||||
<div class="form-actions">
|
||||
<NuxtLink
|
||||
to="/admin/series-management"
|
||||
class="btn"
|
||||
>
|
||||
Cancel
|
||||
</NuxtLink>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
|
||||
<div class="form-actions-right">
|
||||
<button
|
||||
type="button"
|
||||
@click="createAndAddEvent"
|
||||
:disabled="creating"
|
||||
class="px-4 py-2 bg-candlelight-600 text-white rounded-lg hover:bg-candlelight-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||
:disabled="creating"
|
||||
class="btn"
|
||||
>
|
||||
{{ creating ? 'Creating...' : 'Create & Add Event' }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="creating"
|
||||
class="px-6 py-2 bg-earth-600 text-white rounded-lg hover:bg-earth-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="creating"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
{{ creating ? 'Creating...' : 'Create Series' }}
|
||||
</button>
|
||||
|
|
@ -150,7 +130,8 @@
|
|||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
layout: 'admin',
|
||||
middleware: 'admin',
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
|
@ -190,22 +171,22 @@ const generateSlugFromTitle = () => {
|
|||
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
|
||||
}
|
||||
|
||||
|
|
@ -214,7 +195,7 @@ const createSeries = async (redirectAfter = true) => {
|
|||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
creating.value = true
|
||||
try {
|
||||
const response = await $fetch('/api/admin/series', {
|
||||
|
|
@ -224,16 +205,16 @@ const createSeries = async (redirectAfter = true) => {
|
|||
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)
|
||||
|
|
@ -260,9 +241,127 @@ const createAndAddEvent = async () => {
|
|||
totalEvents: seriesForm.totalEvents
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sessionStorage.setItem('seriesEventData', JSON.stringify(seriesData))
|
||||
router.push('/admin/events/create?series=true')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.create-form {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 28px 28px 20px;
|
||||
border-bottom: 1px dashed var(--border);
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-family: 'Brygada 1918', serif;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: var(--text-bright);
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.page-header p { font-size: 12px; color: var(--text-dim); }
|
||||
|
||||
.back-link {
|
||||
font-size: 12px;
|
||||
color: var(--candle);
|
||||
text-decoration: none;
|
||||
margin-bottom: 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
.back-link:hover { text-decoration: underline; }
|
||||
|
||||
.form-body { padding: 24px 28px; }
|
||||
|
||||
.form-section { margin-bottom: 32px; }
|
||||
|
||||
.section-heading {
|
||||
font-family: 'Brygada 1918', serif;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--text-bright);
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px dashed var(--border);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.error-box {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 16px 20px;
|
||||
border: 1px dashed var(--ember);
|
||||
margin-bottom: 20px;
|
||||
font-size: 12px;
|
||||
color: var(--ember);
|
||||
}
|
||||
|
||||
.error-box ul { margin-top: 6px; padding: 0; list-style: none; }
|
||||
.error-box li::before { content: '\2022\00a0'; }
|
||||
|
||||
.success-box {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 16px 20px;
|
||||
border: 1px dashed var(--candle);
|
||||
margin-bottom: 20px;
|
||||
font-size: 12px;
|
||||
color: var(--candle);
|
||||
}
|
||||
|
||||
.box-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.required { color: var(--ember); }
|
||||
|
||||
.has-error { border-color: var(--ember) !important; }
|
||||
|
||||
.help-text { font-size: 11px; color: var(--text-faint); margin-top: 4px; }
|
||||
.field-error { font-size: 11px; color: var(--ember); margin-top: 4px; }
|
||||
|
||||
.slug-display {
|
||||
padding: 5px 8px;
|
||||
font-family: 'Commit Mono', monospace;
|
||||
font-size: 13px;
|
||||
color: var(--text-bright);
|
||||
background: var(--surface);
|
||||
border: 1px dashed var(--border);
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 20px;
|
||||
border-top: 1px dashed var(--border);
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.form-actions-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header { padding: 24px 20px 16px; }
|
||||
.form-body { padding: 20px; }
|
||||
.form-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue