Switch UI components to new design system tokens

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
This commit is contained in:
Jennie Robinson Faber 2025-10-13 15:05:29 +01:00
parent 9b45652b83
commit 3fea484585
13 changed files with 788 additions and 785 deletions

View file

@ -1,62 +1,63 @@
<template>
<div class="space-y-2">
<div class="relative">
<input
v-model="naturalInput"
type="text"
<UInput
v-model="naturalInput"
: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()
}
]"
:color="
hasError && naturalInput.trim()
? 'error'
: isValidParse && naturalInput.trim()
? 'success'
: undefined
"
@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>
>
<template #trailing>
<Icon
v-if="isValidParse && naturalInput.trim()"
name="heroicons:check-circle"
class="w-5 h-5 text-green-500"
/>
<Icon
v-else-if="hasError && naturalInput.trim()"
name="heroicons:exclamation-circle"
class="w-5 h-5 text-red-500"
/>
</template>
</UInput>
</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
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
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">
<summary class="cursor-pointer text-muted hover:text-default">
Use traditional date picker
</summary>
<div class="mt-2">
<input
v-model="datetimeValue"
<UInput
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>
@ -65,174 +66,179 @@
</template>
<script setup>
import * as chrono from 'chrono-node'
import * as chrono from "chrono-node";
const props = defineProps({
modelValue: {
type: String,
default: ''
default: "",
},
placeholder: {
type: String,
default: 'e.g., "tomorrow at 3pm", "next Friday at 9am", "in 2 hours"'
default: 'e.g., "tomorrow at 3pm", "next Friday at 9am", "in 2 hours"',
},
inputClass: {
type: String,
default: ''
default: "",
},
required: {
type: Boolean,
default: false
}
})
default: false,
},
});
const emit = defineEmits(['update:modelValue'])
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('')
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)
const date = new Date(props.modelValue);
if (!isNaN(date.getTime())) {
parsedDate.value = date
datetimeValue.value = formatForDatetimeLocal(date)
isValidParse.value = true
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
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();
}
} else if (!newValue) {
reset()
}
})
},
);
const parseNaturalInput = () => {
const input = naturalInput.value.trim()
const input = naturalInput.value.trim();
if (!input) {
reset()
return
reset();
return;
}
try {
// Parse with chrono-node
const results = chrono.parse(input)
const results = chrono.parse(input);
if (results.length > 0) {
const result = results[0]
const date = result.date()
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))
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')
setError("Could not parse this date format");
}
} else {
setError('Could not understand this date format. Try something like "tomorrow at 3pm" or "next Friday"')
setError(
'Could not understand this date format. Try something like "tomorrow at 3pm" or "next Friday"',
);
}
} catch (error) {
setError('Error parsing date')
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()
parseNaturalInput();
}
}
};
const onDatetimeChange = () => {
if (datetimeValue.value) {
const date = new Date(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)
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()
reset();
}
}
};
const reset = () => {
parsedDate.value = null
isValidParse.value = false
hasError.value = false
errorMessage.value = ''
emit('update:modelValue', '')
}
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
}
isValidParse.value = false;
hasError.value = true;
errorMessage.value = message;
parsedDate.value = null;
};
const formatForDatetimeLocal = (date) => {
if (!date) return ''
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 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 (!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}`
return `Today at ${timeStr}`;
} else if (isTomorrow) {
return `Tomorrow at ${timeStr}`
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
})
return date.toLocaleString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: true,
});
}
}
</script>
};
</script>

View file

@ -1,13 +1,13 @@
<template>
<div class="min-h-screen bg-gray-50">
<div class="min-h-screen bg-muted">
<!-- Admin Navigation -->
<nav class="bg-white border-b border-gray-200 shadow-sm">
<nav class="bg-elevated border-b border-default shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center py-4">
<div class="flex items-center gap-8">
<NuxtLink
to="/"
class="text-xl font-bold text-gray-900 hover:text-primary"
class="text-xl font-bold text-highlighted hover:text-primary"
>
Ghost Guild
</NuxtLink>
@ -18,8 +18,8 @@
:class="[
'px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200',
$route.path === '/admin'
? 'bg-primary-100 text-primary-700 shadow-sm'
: 'text-gray-600 hover:text-primary hover:bg-primary-50',
? 'bg-primary/10 text-primary shadow-sm'
: 'text-muted hover:text-primary hover:bg-primary/5',
]"
>
<svg
@ -49,8 +49,8 @@
:class="[
'px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200',
$route.path.includes('/admin/members')
? 'bg-primary-100 text-primary-700 shadow-sm'
: 'text-gray-600 hover:text-primary hover:bg-primary-50',
? 'bg-primary/10 text-primary shadow-sm'
: 'text-muted hover:text-primary hover:bg-primary/5',
]"
>
<svg
@ -74,8 +74,8 @@
:class="[
'px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200',
$route.path.includes('/admin/events')
? 'bg-primary-100 text-primary-700 shadow-sm'
: 'text-gray-600 hover:text-primary hover:bg-primary-50',
? 'bg-primary/10 text-primary shadow-sm'
: 'text-muted hover:text-primary hover:bg-primary/5',
]"
>
<svg
@ -99,8 +99,8 @@
:class="[
'px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200',
$route.path.includes('/admin/series')
? 'bg-primary-100 text-primary-700 shadow-sm'
: 'text-gray-600 hover:text-primary hover:bg-primary-50',
? 'bg-primary/10 text-primary shadow-sm'
: 'text-muted hover:text-primary hover:bg-primary/5',
]"
>
<svg
@ -129,7 +129,7 @@
v-click-outside="() => (showUserMenu = false)"
>
<button
class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-100 cursor-pointer transition-colors"
class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-muted cursor-pointer transition-colors"
>
<div
class="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center"
@ -148,7 +148,7 @@
/>
</svg>
</div>
<span class="hidden md:block text-sm font-medium text-gray-700"
<span class="hidden md:block text-sm font-medium text-default"
>Admin</span
>
<svg
@ -169,11 +169,11 @@
<!-- User Menu Dropdown -->
<div
v-if="showUserMenu"
class="absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50"
class="absolute right-0 mt-2 w-56 bg-elevated rounded-lg shadow-lg border border-default py-1 z-50"
>
<NuxtLink
to="/"
class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
class="flex items-center px-4 py-2 text-sm text-default hover:bg-muted"
>
<svg
class="w-4 h-4 mr-3 text-gray-400"
@ -198,7 +198,7 @@
</NuxtLink>
<NuxtLink
to="/admin/settings"
class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
class="flex items-center px-4 py-2 text-sm text-default hover:bg-muted"
>
<svg
class="w-4 h-4 mr-3 text-gray-400"
@ -221,7 +221,7 @@
</svg>
Settings
</NuxtLink>
<hr class="my-1 border-gray-200" />
<hr class="my-1 border-default" />
<button
@click="logout"
class="flex items-center w-full px-4 py-2 text-sm text-red-600 hover:bg-red-50"
@ -249,7 +249,7 @@
</nav>
<!-- Mobile Navigation -->
<div class="md:hidden bg-white border-b border-gray-200">
<div class="md:hidden bg-elevated border-b border-default">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center gap-2 py-3 overflow-x-auto">
<NuxtLink
@ -257,8 +257,8 @@
:class="[
'px-3 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-colors',
$route.path === '/admin'
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:text-primary hover:bg-primary-50',
? 'bg-primary/10 text-primary'
: 'text-muted hover:text-primary hover:bg-primary/5',
]"
>
Dashboard
@ -269,8 +269,8 @@
:class="[
'px-3 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-colors',
$route.path.includes('/admin/members')
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:text-primary hover:bg-primary-50',
? 'bg-primary/10 text-primary'
: 'text-muted hover:text-primary hover:bg-primary/5',
]"
>
Members
@ -281,8 +281,8 @@
:class="[
'px-3 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-colors',
$route.path.includes('/admin/events')
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:text-primary hover:bg-primary-50',
? 'bg-primary/10 text-primary'
: 'text-muted hover:text-primary hover:bg-primary/5',
]"
>
Events
@ -293,8 +293,8 @@
:class="[
'px-3 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-colors',
$route.path.includes('/admin/series')
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:text-primary hover:bg-primary-50',
? 'bg-primary/10 text-primary'
: 'text-muted hover:text-primary hover:bg-primary/5',
]"
>
Series
@ -309,10 +309,10 @@
</main>
<!-- Footer -->
<footer class="bg-white border-t border-gray-200 mt-auto">
<footer class="bg-elevated border-t border-default mt-auto">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="py-8 text-center">
<p class="text-sm text-gray-600">
<p class="text-sm text-muted">
&copy; 2025 Ghost Guild. Admin Panel.
</p>
</div>

View file

@ -1,10 +1,10 @@
<template>
<div>
<div class="bg-white border-b">
<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-gray-900">Admin Dashboard</h1>
<p class="text-gray-600">
<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>
@ -14,10 +14,10 @@
<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="bg-elevated 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-sm text-muted">Total Members</p>
<p class="text-2xl font-bold text-blue-600">
{{ stats.totalMembers || 0 }}
</p>
@ -42,10 +42,10 @@
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="bg-elevated 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-sm text-muted">Active Events</p>
<p class="text-2xl font-bold text-green-600">
{{ stats.activeEvents || 0 }}
</p>
@ -70,10 +70,10 @@
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="bg-elevated 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-sm text-muted">Monthly Revenue</p>
<p class="text-2xl font-bold text-purple-600">
${{ stats.monthlyRevenue || 0 }}
</p>
@ -98,10 +98,10 @@
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="bg-elevated 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-sm text-muted">Pending Slack Invites</p>
<p class="text-2xl font-bold text-orange-600">
{{ stats.pendingSlackInvites || 0 }}
</p>
@ -128,8 +128,8 @@
</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="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"
@ -148,8 +148,10 @@
></path>
</svg>
</div>
<h3 class="text-lg font-semibold mb-2">Add New Member</h3>
<p class="text-gray-600 text-sm mb-4">
<h3 class="text-lg font-semibold text-highlighted mb-2">
Add New Member
</h3>
<p class="text-muted text-sm mb-4">
Add a new member to the Ghost Guild community
</p>
<button
@ -161,7 +163,7 @@
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<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"
@ -180,8 +182,10 @@
></path>
</svg>
</div>
<h3 class="text-lg font-semibold mb-2">Create Event</h3>
<p class="text-gray-600 text-sm mb-4">
<h3 class="text-lg font-semibold text-highlighted mb-2">
Create Event
</h3>
<p class="text-muted text-sm mb-4">
Schedule a new community event or workshop
</p>
<button
@ -192,49 +196,19 @@
</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="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">Recent Members</h3>
<h3 class="text-lg font-semibold text-highlighted">
Recent Members
</h3>
<button
@click="navigateTo('/admin/members-working')"
class="text-sm text-primary-600 hover:text-primary-900"
class="text-sm text-primary hover:text-primary"
>
View All
</button>
@ -251,11 +225,15 @@
<div
v-for="member in recentMembers"
:key="member._id"
class="flex items-center justify-between p-3 rounded-lg border border-gray-200"
class="flex items-center justify-between p-3 rounded-lg border border-default"
>
<div>
<p class="font-medium">{{ member.name }}</p>
<p class="text-sm text-gray-600">{{ member.email }}</p>
<p class="font-medium text-highlighted">
{{ member.name }}
</p>
<p class="text-sm text-muted">
{{ member.email }}
</p>
</div>
<div class="text-right">
<span
@ -264,25 +242,27 @@
>
{{ member.circle }}
</span>
<p class="text-xs text-gray-500">
<p class="text-xs text-dimmed">
{{ formatDate(member.createdAt) }}
</p>
</div>
</div>
</div>
<div v-else class="text-center py-6 text-gray-500">
<div v-else class="text-center py-6 text-dimmed">
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="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">Upcoming Events</h3>
<h3 class="text-lg font-semibold text-highlighted">
Upcoming Events
</h3>
<button
@click="navigateTo('/admin/events-working')"
class="text-sm text-primary-600 hover:text-primary-900"
class="text-sm text-primary hover:text-primary"
>
View All
</button>
@ -299,11 +279,13 @@
<div
v-for="event in upcomingEvents"
:key="event._id"
class="flex items-center justify-between p-3 rounded-lg border border-gray-200"
class="flex items-center justify-between p-3 rounded-lg border border-default"
>
<div>
<p class="font-medium">{{ event.title }}</p>
<p class="text-sm text-gray-600">
<p class="font-medium text-highlighted">
{{ event.title }}
</p>
<p class="text-sm text-muted">
{{ formatDateTime(event.startDate) }}
</p>
</div>
@ -314,13 +296,13 @@
>
{{ event.eventType }}
</span>
<p class="text-xs text-gray-500">
<p class="text-xs text-dimmed">
{{ event.location || "Online" }}
</p>
</div>
</div>
</div>
<div v-else class="text-center py-6 text-gray-500">
<div v-else class="text-center py-6 text-dimmed">
No upcoming events
</div>
</div>

View file

@ -1,10 +1,10 @@
<template>
<div>
<div class="bg-white border-b">
<div class="bg-elevated 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">
<h1 class="text-2xl font-bold text-highlighted">Event Management</h1>
<p class="text-muted">
Create, manage, and monitor Ghost Guild events and workshops
</p>
</div>
@ -15,30 +15,32 @@
<!-- Search and Actions -->
<div class="mb-6 flex justify-between items-center">
<div class="flex gap-4 items-center">
<input
<UInput
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"
class="w-80"
/>
<select
<USelect
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
:items="[
{ label: 'All Types', value: '' },
{ label: 'Community', value: 'community' },
{ label: 'Workshop', value: 'workshop' },
{ label: 'Social', value: 'social' },
{ label: 'Showcase', value: 'showcase' },
]"
class="w-full"
/>
<USelect
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>
:items="[
{ label: 'All Status', value: '' },
{ label: 'Upcoming', value: 'upcoming' },
{ label: 'Ongoing', value: 'ongoing' },
{ label: 'Past', value: 'past' },
]"
class="w-full"
/>
</div>
<button
@click="showCreateModal = true"
@ -49,7 +51,7 @@
</div>
<!-- Events Table -->
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="bg-elevated rounded-lg shadow overflow-hidden">
<div v-if="pending" class="p-8 text-center">
<div class="inline-flex items-center">
<div
@ -64,51 +66,51 @@
</div>
<table v-else class="w-full">
<thead class="bg-gray-50">
<thead class="bg-muted">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
>
Title
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
>
Type
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
>
Start Date
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
>
Status
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
>
Registration
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
>
Actions
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tbody class="bg-elevated divide-y divide-default">
<tr
v-for="event in filteredEvents"
:key="event._id"
class="hover:bg-gray-50"
class="hover:bg-muted"
>
<td class="px-6 py-4">
<div class="text-sm font-medium text-gray-900">
<div class="text-sm font-medium text-highlighted">
{{ event.title }}
</div>
<div class="text-sm text-gray-500">
<div class="text-sm text-dimmed">
{{ event.description.substring(0, 100) }}...
</div>
</td>
@ -120,7 +122,7 @@
{{ event.eventType }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
<td class="px-6 py-4 whitespace-nowrap text-sm text-muted">
{{ formatDateTime(event.startDate) }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
@ -136,24 +138,24 @@
:class="
event.registrationRequired
? 'bg-blue-100 text-blue-800'
: 'bg-gray-100 text-gray-800'
: 'bg-accented text-default'
"
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">
<td class="px-6 py-4 whitespace-nowrap text-sm text-default">
<div class="flex gap-2">
<button
@click="editEvent(event)"
class="text-primary-600 hover:text-primary-900"
class="text-primary hover:text-primary"
>
Edit
</button>
<button
@click="duplicateEvent(event)"
class="text-primary-600 hover:text-primary-900"
class="text-primary hover:text-primary"
>
Duplicate
</button>
@ -171,7 +173,7 @@
<div
v-if="!pending && !error && filteredEvents.length === 0"
class="p-8 text-center text-gray-500"
class="p-8 text-center text-dimmed"
>
No events found matching your criteria
</div>
@ -183,7 +185,7 @@
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="bg-elevated 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" }}
@ -193,115 +195,116 @@
<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"
<label class="block text-sm font-medium text-default mb-1"
>Event Title</label
>
<input
<UInput
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"
class="w-full"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1"
<label class="block text-sm font-medium text-default mb-1"
>Event Type</label
>
<select
<USelect
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"
:items="[
{ label: 'Community Meetup', value: 'community' },
{ label: 'Workshop', value: 'workshop' },
{ label: 'Social Event', value: 'social' },
{ label: 'Showcase', value: 'showcase' },
]"
class="w-full"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1"
<label class="block text-sm font-medium text-default mb-1"
>Location</label
>
<UInput
v-model="eventForm.location"
placeholder="Event location"
class="w-full"
/>
</div>
<div>
<label class="block text-sm font-medium text-default mb-1"
>Start Date & Time</label
>
<input
<UInput
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"
class="w-full"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1"
<label class="block text-sm font-medium text-default mb-1"
>End Date & Time</label
>
<input
<UInput
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"
class="w-full"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1"
<label class="block text-sm font-medium text-default mb-1"
>Max Attendees</label
>
<input
<UInput
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"
class="w-full"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1"
<label class="block text-sm font-medium text-default mb-1"
>Registration Deadline</label
>
<input
<UInput
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"
class="w-full"
/>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1"
<label class="block text-sm font-medium text-default mb-1"
>Description</label
>
<textarea
<UTextarea
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>
:rows="3"
class="w-full"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1"
<label class="block text-sm font-medium text-default mb-1"
>Additional Content</label
>
<textarea
<UTextarea
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>
:rows="4"
class="w-full"
/>
</div>
<div class="flex items-center gap-6">
@ -309,17 +312,17 @@
<input
v-model="eventForm.isOnline"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
class="rounded border-default text-blue-600 focus:ring-blue-500"
/>
<span class="ml-2 text-sm text-gray-700">Online Event</span>
<span class="ml-2 text-sm text-default">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"
class="rounded border-default text-blue-600 focus:ring-blue-500"
/>
<span class="ml-2 text-sm text-gray-700"
<span class="ml-2 text-sm text-default"
>Registration Required</span
>
</label>
@ -329,7 +332,7 @@
<button
type="button"
@click="cancelEdit"
class="px-4 py-2 text-gray-600 hover:text-gray-900"
class="px-4 py-2 text-muted hover:text-highlighted"
>
Cancel
</button>

View file

@ -1,20 +1,17 @@
<template>
<div>
<div class="bg-white border-b">
<div class="bg-elevated border-b">
<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/events"
class="text-gray-500 hover:text-gray-700"
>
<NuxtLink to="/admin/events" class="text-dimmed hover:text-default">
<Icon name="heroicons:arrow-left" class="w-5 h-5" />
</NuxtLink>
<h1 class="text-2xl font-bold text-gray-900">
<h1 class="text-2xl font-bold text-highlighted">
{{ editingEvent ? "Edit Event" : "Create New Event" }}
</h1>
</div>
<p class="text-gray-600">
<p class="text-muted">
Fill out the form below to create or update an event
</p>
</div>
@ -68,24 +65,21 @@
<form @submit.prevent="saveEvent">
<!-- Basic Information -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">
<h2 class="text-lg font-semibold text-highlighted mb-4">
Basic Information
</h2>
<div class="grid grid-cols-1 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
<label class="block text-sm font-medium text-default mb-2">
Event Title <span class="text-red-500">*</span>
</label>
<input
<UInput
v-model="eventForm.title"
type="text"
placeholder="Enter a clear, descriptive 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"
:class="{
'border-red-300 focus:ring-red-500': fieldErrors.title,
}"
:color="fieldErrors.title ? 'error' : undefined"
class="w-full"
/>
<p v-if="fieldErrors.title" class="mt-1 text-sm text-red-600">
{{ fieldErrors.title }}
@ -93,52 +87,50 @@
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"
<label class="block text-sm font-medium text-default mb-2"
>Feature Image</label
>
<ImageUpload v-model="eventForm.featureImage" />
<p class="mt-1 text-sm text-gray-500">
<p class="mt-1 text-sm text-dimmed">
Upload a high-quality image (1200x630px recommended) to
represent your event
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
<label class="block text-sm font-medium text-default mb-2">
Event Description <span class="text-red-500">*</span>
</label>
<textarea
<UTextarea
v-model="eventForm.description"
placeholder="Provide a clear description of what attendees can expect from this event"
required
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"
:class="{
'border-red-300 focus:ring-red-500': fieldErrors.description,
}"
></textarea>
:rows="4"
:color="fieldErrors.description ? 'error' : undefined"
class="w-full"
/>
<p
v-if="fieldErrors.description"
class="mt-1 text-sm text-red-600"
>
{{ fieldErrors.description }}
</p>
<p class="mt-1 text-sm text-gray-500">
<p class="mt-1 text-sm text-dimmed">
This will be displayed on the event listing and detail pages
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"
<label class="block text-sm font-medium text-default mb-2"
>Additional Content</label
>
<textarea
<UTextarea
v-model="eventForm.content"
placeholder="Add detailed information, agenda, requirements, or other important details"
rows="6"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
></textarea>
<p class="mt-1 text-sm text-gray-500">
:rows="6"
class="w-full"
/>
<p class="mt-1 text-sm text-dimmed">
Optional: Provide additional context, agenda items, or detailed
requirements
</p>
@ -148,53 +140,51 @@
<!-- Event Details -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">
<h2 class="text-lg font-semibold text-highlighted mb-4">
Event Details
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
<label class="block text-sm font-medium text-default mb-2">
Event Type <span class="text-red-500">*</span>
</label>
<select
<USelect
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>
<p class="mt-1 text-sm text-gray-500">
:items="[
{ label: 'Community Meetup', value: 'community' },
{ label: 'Workshop', value: 'workshop' },
{ label: 'Social Event', value: 'social' },
{ label: 'Showcase', value: 'showcase' },
]"
class="w-full"
/>
<p class="mt-1 text-sm text-dimmed">
Choose the category that best describes your event
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
<label class="block text-sm font-medium text-default mb-2">
Location <span class="text-red-500">*</span>
</label>
<input
<UInput
v-model="eventForm.location"
type="text"
placeholder="e.g., https://zoom.us/j/123... or #channel-name"
required
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
:class="{
'border-red-300 focus:ring-red-500': fieldErrors.location,
}"
:color="fieldErrors.location ? 'error' : undefined"
class="w-full"
/>
<p v-if="fieldErrors.location" class="mt-1 text-sm text-red-600">
{{ fieldErrors.location }}
</p>
<p class="mt-1 text-sm text-gray-500">
<p class="mt-1 text-sm text-dimmed">
Enter a video conference link or Slack channel (starting with #)
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
<label class="block text-sm font-medium text-default mb-2">
Start Date & Time <span class="text-red-500">*</span>
</label>
<NaturalDateInput
@ -211,7 +201,7 @@
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
<label class="block text-sm font-medium text-default mb-2">
End Date & Time <span class="text-red-500">*</span>
</label>
<NaturalDateInput
@ -228,30 +218,30 @@
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"
<label class="block text-sm font-medium text-default mb-2"
>Max Attendees</label
>
<input
<UInput
v-model="eventForm.maxAttendees"
type="number"
min="1"
placeholder="Leave blank for unlimited"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="w-full"
/>
<p class="mt-1 text-sm text-gray-500">
<p class="mt-1 text-sm text-dimmed">
Set a maximum number of attendees (optional)
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"
<label class="block text-sm font-medium text-default mb-2"
>Registration Deadline</label
>
<NaturalDateInput
v-model="eventForm.registrationDeadline"
placeholder="e.g., 'tomorrow at noon', '1 hour before event'"
/>
<p class="mt-1 text-sm text-gray-500">
<p class="mt-1 text-sm text-dimmed">
When should registration close? (optional)
</p>
</div>
@ -260,12 +250,12 @@
<!-- Target Audience -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">
<h2 class="text-lg font-semibold text-highlighted mb-4">
Target Audience
</h2>
<div>
<label class="block text-sm font-medium text-gray-700 mb-3"
<label class="block text-sm font-medium text-default mb-3"
>Target Circles</label
>
<div class="space-y-3">
@ -274,13 +264,13 @@
v-model="eventForm.targetCircles"
value="community"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700"
<span class="text-sm font-medium text-default"
>Community Circle</span
>
<p class="text-xs text-gray-500">
<p class="text-xs text-dimmed">
New members and those exploring the community
</p>
</div>
@ -290,13 +280,13 @@
v-model="eventForm.targetCircles"
value="founder"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700"
<span class="text-sm font-medium text-default"
>Founder Circle</span
>
<p class="text-xs text-gray-500">
<p class="text-xs text-dimmed">
Entrepreneurs and business leaders
</p>
</div>
@ -306,19 +296,19 @@
v-model="eventForm.targetCircles"
value="practitioner"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700"
<span class="text-sm font-medium text-default"
>Practitioner Circle</span
>
<p class="text-xs text-gray-500">
<p class="text-xs text-dimmed">
Experts and professionals sharing knowledge
</p>
</div>
</label>
</div>
<p class="mt-2 text-sm text-gray-500">
<p class="mt-2 text-sm text-dimmed">
Select which circles this event is most relevant for (leave blank
for all circles)
</p>
@ -327,20 +317,20 @@
<!-- Ticketing -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Ticketing</h2>
<h2 class="text-lg font-semibold text-highlighted mb-4">Ticketing</h2>
<div class="space-y-6">
<label class="flex items-start">
<input
v-model="eventForm.tickets.enabled"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700"
<span class="text-sm font-medium text-default"
>Enable Ticketing</span
>
<p class="text-xs text-gray-500">
<p class="text-xs text-dimmed">
Allow ticket sales for this event
</p>
</div>
@ -348,19 +338,19 @@
<div
v-if="eventForm.tickets.enabled"
class="ml-6 space-y-4 p-4 bg-gray-50 rounded-lg"
class="ml-6 space-y-4 p-4 bg-muted rounded-lg"
>
<label class="flex items-start">
<input
v-model="eventForm.tickets.public.available"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700"
<span class="text-sm font-medium text-default"
>Public Tickets Available</span
>
<p class="text-xs text-gray-500">
<p class="text-xs text-dimmed">
Allow non-members to purchase tickets
</p>
</div>
@ -369,78 +359,77 @@
<div v-if="eventForm.tickets.public.available" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"
<label class="block text-sm font-medium text-default mb-2"
>Ticket Name</label
>
<input
<UInput
v-model="eventForm.tickets.public.name"
type="text"
placeholder="e.g., General Admission"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="w-full"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"
<label class="block text-sm font-medium text-default mb-2"
>Price (CAD)</label
>
<input
<UInput
v-model="eventForm.tickets.public.price"
type="number"
min="0"
step="0.01"
placeholder="0.00"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="w-full"
/>
<p class="mt-1 text-xs text-gray-500">
<p class="mt-1 text-xs text-dimmed">
Set to 0 for free public events
</p>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"
<label class="block text-sm font-medium text-default mb-2"
>Ticket Description</label
>
<textarea
<UTextarea
v-model="eventForm.tickets.public.description"
placeholder="What's included with this ticket..."
rows="2"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
></textarea>
:rows="2"
class="w-full"
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"
<label class="block text-sm font-medium text-default mb-2"
>Quantity Available</label
>
<input
<UInput
v-model="eventForm.tickets.public.quantity"
type="number"
min="1"
placeholder="Leave blank for unlimited"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="w-full"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"
<label class="block text-sm font-medium text-default mb-2"
>Early Bird Price (Optional)</label
>
<input
<UInput
v-model="eventForm.tickets.public.earlyBirdPrice"
type="number"
min="0"
step="0.01"
placeholder="0.00"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="w-full"
/>
</div>
</div>
<div v-if="eventForm.tickets.public.earlyBirdPrice > 0">
<label class="block text-sm font-medium text-gray-700 mb-2"
<label class="block text-sm font-medium text-default mb-2"
>Early Bird Deadline</label
>
<div class="md:w-1/2">
@ -449,7 +438,7 @@
placeholder="e.g., '1 week before event', 'next Monday'"
/>
</div>
<p class="mt-1 text-xs text-gray-500">
<p class="mt-1 text-xs text-dimmed">
Price increases to regular price after this date
</p>
</div>
@ -467,7 +456,7 @@
<!-- Series Management -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">
<h2 class="text-lg font-semibold text-highlighted mb-4">
Series Management
</h2>
@ -476,13 +465,13 @@
<input
v-model="eventForm.series.isSeriesEvent"
type="checkbox"
class="rounded border-gray-300 text-purple-600 focus:ring-purple-500 mt-1"
class="rounded border-default text-purple-600 focus:ring-purple-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700"
<span class="text-sm font-medium text-default"
>Part of Event Series</span
>
<p class="text-xs text-gray-500">
<p class="text-xs text-dimmed">
This event is part of a multi-event series
</p>
</div>
@ -490,29 +479,26 @@
<div
v-if="eventForm.series.isSeriesEvent"
class="ml-6 space-y-4 p-4 bg-purple-50 rounded-lg border border-purple-200"
class="ml-6 space-y-4 p-4 bg-purple-500/10 rounded-lg border border-purple-500/20"
>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
<label class="block text-sm font-medium text-default mb-2">
Select Series <span class="text-red-500">*</span>
</label>
<div class="flex gap-2">
<select
<USelect
v-model="selectedSeriesId"
@change="onSeriesSelect"
class="flex-1 border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="">
Choose existing series or create new...
</option>
<option
v-for="series in availableSeries"
:key="series.id"
:value="series.id"
>
{{ series.title }} ({{ series.eventCount || 0 }} events)
</option>
</select>
@update:model-value="onSeriesSelect"
:items="
availableSeries.map((series) => ({
label: `${series.title} (${series.eventCount || 0} events)`,
value: series.id,
}))
"
placeholder="Choose existing series or create new..."
value-key="value"
class="flex-1 w-full"
/>
<NuxtLink
to="/admin/series/create"
class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm font-medium whitespace-nowrap"
@ -520,7 +506,7 @@
New Series
</NuxtLink>
</div>
<p class="text-xs text-gray-500 mt-1">
<p class="text-xs text-dimmed mt-1">
Select an existing series or create a new one
</p>
</div>
@ -530,19 +516,18 @@
class="space-y-4"
>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
<label class="block text-sm font-medium text-default mb-2">
Series Title <span class="text-red-500">*</span>
</label>
<input
<UInput
v-model="eventForm.series.title"
type="text"
placeholder="e.g., Cooperative Game Development Fundamentals"
required
:readonly="selectedSeriesId"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
:class="{ 'bg-gray-100': selectedSeriesId }"
:class="{ 'bg-accented': selectedSeriesId }"
class="w-full"
/>
<p class="text-xs text-gray-500 mt-1">
<p class="text-xs text-dimmed mt-1">
{{
selectedSeriesId
? "From selected series"
@ -552,19 +537,19 @@
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
<label class="block text-sm font-medium text-default mb-2">
Series Description <span class="text-red-500">*</span>
</label>
<textarea
<UTextarea
v-model="eventForm.series.description"
placeholder="Describe what the series covers and its goals"
required
rows="3"
:rows="3"
:readonly="selectedSeriesId"
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
:class="{ 'bg-gray-100': selectedSeriesId }"
></textarea>
<p class="text-xs text-gray-500 mt-1">
:class="{ 'bg-accented': selectedSeriesId }"
class="w-full"
/>
<p class="text-xs text-dimmed mt-1">
{{
selectedSeriesId
? "From selected series"
@ -573,8 +558,11 @@
</p>
</div>
<div v-if="selectedSeriesId" class="p-3 bg-blue-50 rounded-lg">
<p class="text-sm text-blue-700">
<div
v-if="selectedSeriesId"
class="p-3 bg-blue-500/10 rounded-lg border border-blue-500/20"
>
<p class="text-sm text-blue-600 dark:text-blue-400">
<strong>Note:</strong> This event will be added to the
existing "{{ eventForm.series.title }}" series.
</p>
@ -586,7 +574,9 @@
<!-- Event Agenda -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Event Agenda</h2>
<h2 class="text-lg font-semibold text-highlighted mb-4">
Event Agenda
</h2>
<div class="space-y-3">
<div
@ -594,11 +584,10 @@
:key="index"
class="flex gap-2"
>
<input
<UInput
v-model="eventForm.agenda[index]"
type="text"
placeholder="Enter agenda item (e.g., 'Introduction and welcome - 10 mins')"
class="flex-1 border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="flex-1 w-full"
/>
<button
type="button"
@ -619,7 +608,7 @@
</button>
</div>
<p class="mt-2 text-sm text-gray-500">
<p class="mt-2 text-sm text-dimmed">
Add agenda items to help attendees know what to expect during the
event
</p>
@ -627,7 +616,7 @@
<!-- Event Settings -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">
<h2 class="text-lg font-semibold text-highlighted mb-4">
Event Settings
</h2>
@ -637,13 +626,13 @@
<input
v-model="eventForm.isOnline"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700"
<span class="text-sm font-medium text-default"
>Online Event</span
>
<p class="text-xs text-gray-500">
<p class="text-xs text-dimmed">
Event will be conducted virtually
</p>
</div>
@ -653,13 +642,13 @@
<input
v-model="eventForm.registrationRequired"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700"
<span class="text-sm font-medium text-default"
>Registration Required</span
>
<p class="text-xs text-gray-500">
<p class="text-xs text-dimmed">
Attendees must register before attending
</p>
</div>
@ -671,13 +660,13 @@
<input
v-model="eventForm.isVisible"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700"
<span class="text-sm font-medium text-default"
>Visible on Public Calendar</span
>
<p class="text-xs text-gray-500">
<p class="text-xs text-dimmed">
Event will appear on the public events page
</p>
</div>
@ -687,13 +676,13 @@
<input
v-model="eventForm.isCancelled"
type="checkbox"
class="rounded border-gray-300 text-red-600 focus:ring-red-500 mt-1"
class="rounded border-default text-red-600 focus:ring-red-500 mt-1"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700"
<span class="text-sm font-medium text-default"
>Event Cancelled</span
>
<p class="text-xs text-gray-500">
<p class="text-xs text-dimmed">
Mark this event as cancelled
</p>
</div>
@ -704,27 +693,28 @@
<!-- Cancellation Message (conditional) -->
<div v-if="eventForm.isCancelled" class="mb-8">
<label class="block text-sm font-medium text-gray-700 mb-2"
<label class="block text-sm font-medium text-default mb-2"
>Cancellation Message</label
>
<textarea
<UTextarea
v-model="eventForm.cancellationMessage"
placeholder="Explain why the event was cancelled and any next steps..."
rows="3"
class="w-full border border-red-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-red-500 focus:border-transparent"
></textarea>
<p class="text-xs text-gray-500 mt-1">
:rows="3"
color="error"
class="w-full"
/>
<p class="text-xs text-dimmed mt-1">
This message will be displayed to users viewing the event page
</p>
</div>
<!-- Form Actions -->
<div
class="flex justify-between items-center pt-6 border-t border-gray-200"
class="flex justify-between items-center pt-6 border-t border-default"
>
<NuxtLink
to="/admin/events"
class="px-4 py-2 text-gray-600 hover:text-gray-900 font-medium"
class="px-4 py-2 text-muted hover:text-highlighted font-medium"
>
Cancel
</NuxtLink>
@ -773,7 +763,7 @@ const editingEvent = ref(null);
const showSuccessMessage = ref(false);
const formErrors = ref([]);
const fieldErrors = ref({});
const selectedSeriesId = ref("");
const selectedSeriesId = ref(null);
const availableSeries = ref([]);
const eventForm = reactive({
@ -827,7 +817,9 @@ const removeAgendaItem = (index) => {
onMounted(async () => {
try {
const response = await $fetch("/api/admin/series");
console.log("Loaded series:", response);
availableSeries.value = response;
console.log("availableSeries.value:", availableSeries.value);
} catch (error) {
console.error("Failed to load series:", error);
}
@ -835,14 +827,21 @@ onMounted(async () => {
// Handle series selection
const onSeriesSelect = () => {
console.log(
"onSeriesSelect called, selectedSeriesId:",
selectedSeriesId.value,
);
console.log("availableSeries:", availableSeries.value);
if (selectedSeriesId.value) {
const series = availableSeries.value.find(
(s) => s.id === selectedSeriesId.value,
);
console.log("Found series:", series);
if (series) {
eventForm.series.id = series.id;
eventForm.series.title = series.title;
eventForm.series.description = series.description;
console.log("Updated eventForm.series:", eventForm.series);
}
} else {
// Reset series form when no series is selected

View file

@ -1,10 +1,10 @@
<template>
<div>
<div class="bg-white border-b">
<div class="bg-elevated 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">
<h1 class="text-2xl font-bold text-highlighted">Event Management</h1>
<p class="text-muted">
Create, manage, and monitor Ghost Guild events and workshops
</p>
</div>
@ -15,38 +15,41 @@
<!-- Search and Actions -->
<div class="mb-6 flex justify-between items-center">
<div class="flex gap-4 items-center">
<input
<UInput
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"
class="w-80"
/>
<select
<USelect
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
:items="[
{ label: 'All Types', value: '' },
{ label: 'Community', value: 'community' },
{ label: 'Workshop', value: 'workshop' },
{ label: 'Social', value: 'social' },
{ label: 'Showcase', value: 'showcase' },
]"
class="w-full"
/>
<USelect
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>
<select
:items="[
{ label: 'All Status', value: '' },
{ label: 'Upcoming', value: 'upcoming' },
{ label: 'Ongoing', value: 'ongoing' },
{ label: 'Past', value: 'past' },
]"
class="w-full"
/>
<USelect
v-model="seriesFilter"
class="border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">All Events</option>
<option value="series-only">Series Events Only</option>
<option value="standalone-only">Standalone Only</option>
</select>
:items="[
{ label: 'All Events', value: '' },
{ label: 'Series Events Only', value: 'series-only' },
{ label: 'Standalone Only', value: 'standalone-only' },
]"
class="w-full"
/>
</div>
<NuxtLink
to="/admin/events/create"
@ -58,7 +61,7 @@
</div>
<!-- Events Table -->
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="bg-elevated rounded-lg shadow overflow-hidden">
<div v-if="pending" class="p-8 text-center">
<div class="inline-flex items-center">
<div
@ -73,45 +76,45 @@
</div>
<table v-else class="w-full">
<thead class="bg-gray-50">
<thead class="bg-muted">
<tr>
<th
class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-6 py-4 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
>
Title
</th>
<th
class="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-4 py-4 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
>
Type
</th>
<th
class="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-4 py-4 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
>
Date
</th>
<th
class="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-4 py-4 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
>
Status
</th>
<th
class="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-4 py-4 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
>
Registration
</th>
<th
class="px-6 py-4 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-6 py-4 text-right text-xs font-medium text-dimmed uppercase tracking-wider"
>
Actions
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tbody class="bg-elevated divide-y divide-default">
<tr
v-for="event in filteredEvents"
:key="event._id"
class="hover:bg-gray-50"
class="hover:bg-muted"
>
<!-- Title Column -->
<td class="px-6 py-6">
@ -120,7 +123,7 @@
v-if="
event.featureImage?.url && !event.featureImage?.publicId
"
class="flex-shrink-0 w-12 h-12 bg-gray-100 rounded-lg overflow-hidden"
class="flex-shrink-0 w-12 h-12 bg-accented rounded-lg overflow-hidden"
>
<img
:src="event.featureImage.url"
@ -131,18 +134,18 @@
</div>
<div
v-else
class="flex-shrink-0 w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center"
class="flex-shrink-0 w-12 h-12 bg-accented rounded-lg flex items-center justify-center"
>
<Icon
name="heroicons:calendar-days"
class="w-6 h-6 text-gray-400"
class="w-6 h-6 text-muted"
/>
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-semibold text-gray-900 mb-1">
<div class="text-sm font-semibold text-highlighted mb-1">
{{ event.title }}
</div>
<div class="text-sm text-gray-500 line-clamp-2">
<div class="text-sm text-dimmed line-clamp-2">
{{ event.description.substring(0, 100) }}...
</div>
<div v-if="event.series?.isSeriesEvent" class="mt-2 mb-2">
@ -176,15 +179,15 @@
>
<Icon
name="heroicons:user-group"
class="w-3 h-3 text-gray-400"
class="w-3 h-3 text-muted"
/>
<span class="text-xs text-gray-500">{{
<span class="text-xs text-dimmed">{{
event.targetCircles.join(", ")
}}</span>
</div>
<div
v-if="!event.isVisible"
class="flex items-center text-xs text-gray-500"
class="flex items-center text-xs text-dimmed"
>
<Icon name="heroicons:eye-slash" class="w-3 h-3 mr-1" />
Hidden
@ -205,12 +208,12 @@
</td>
<!-- Date Column -->
<td class="px-4 py-6 whitespace-nowrap text-sm text-gray-600">
<td class="px-4 py-6 whitespace-nowrap text-sm text-muted">
<div class="space-y-1">
<div class="font-medium">
{{ formatDate(event.startDate) }}
</div>
<div class="text-xs text-gray-500">
<div class="text-xs text-dimmed">
{{ formatTime(event.startDate) }}
</div>
</div>
@ -245,11 +248,11 @@
</div>
<div
v-else
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-gray-100 text-gray-800"
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-accented text-default"
>
Optional
</div>
<div v-if="event.maxAttendees" class="text-xs text-gray-500">
<div v-if="event.maxAttendees" class="text-xs text-dimmed">
{{ event.registeredCount || 0 }} / {{ event.maxAttendees }}
</div>
</div>
@ -260,21 +263,21 @@
<div class="flex items-center justify-end space-x-2">
<NuxtLink
:to="`/events/${event.slug || String(event._id)}`"
class="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-full transition-colors"
class="p-2 text-dimmed hover:text-default hover:bg-accented rounded-full transition-colors"
title="View Event"
>
<Icon name="heroicons:eye" class="w-4 h-4" />
</NuxtLink>
<button
@click="editEvent(event)"
class="p-2 text-primary-500 hover:text-primary-700 hover:bg-primary-50 rounded-full transition-colors"
class="p-2 text-primary hover:text-primary hover:bg-primary/10 rounded-full transition-colors"
title="Edit Event"
>
<Icon name="heroicons:pencil-square" class="w-4 h-4" />
</button>
<button
@click="duplicateEvent(event)"
class="p-2 text-primary-500 hover:text-primary-700 hover:bg-primary-50 rounded-full transition-colors"
class="p-2 text-primary hover:text-primary hover:bg-primary/10 rounded-full transition-colors"
title="Duplicate Event"
>
<Icon name="heroicons:document-duplicate" class="w-4 h-4" />
@ -294,7 +297,7 @@
<div
v-if="!pending && !error && filteredEvents.length === 0"
class="p-8 text-center text-gray-500"
class="p-8 text-center text-dimmed"
>
No events found matching your criteria
</div>

View file

@ -1,10 +1,10 @@
<template>
<div>
<div class="bg-white border-b">
<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-gray-900">Admin Dashboard</h1>
<p class="text-gray-600">
<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>
@ -14,10 +14,10 @@
<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="bg-elevated 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-sm text-muted">Total Members</p>
<p class="text-2xl font-bold text-blue-600">
{{ stats.totalMembers || 0 }}
</p>
@ -42,10 +42,10 @@
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="bg-elevated 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-sm text-muted">Active Events</p>
<p class="text-2xl font-bold text-green-600">
{{ stats.activeEvents || 0 }}
</p>
@ -70,10 +70,10 @@
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="bg-elevated 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-sm text-muted">Monthly Revenue</p>
<p class="text-2xl font-bold text-purple-600">
${{ stats.monthlyRevenue || 0 }}
</p>
@ -98,10 +98,10 @@
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="bg-elevated 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-sm text-muted">Pending Slack Invites</p>
<p class="text-2xl font-bold text-orange-600">
{{ stats.pendingSlackInvites || 0 }}
</p>
@ -128,8 +128,8 @@
</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="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"
@ -148,8 +148,10 @@
></path>
</svg>
</div>
<h3 class="text-lg font-semibold mb-2">Add New Member</h3>
<p class="text-gray-600 text-sm mb-4">
<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
@ -161,7 +163,7 @@
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<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"
@ -180,8 +182,10 @@
></path>
</svg>
</div>
<h3 class="text-lg font-semibold mb-2">Create Event</h3>
<p class="text-gray-600 text-sm mb-4">
<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
@ -192,49 +196,19 @@
</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="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">Recent Members</h3>
<h3 class="text-lg font-semibold text-highlighted">
Recent Members
</h3>
<button
@click="navigateTo('/admin/members')"
class="text-sm text-primary-600 hover:text-primary-900"
class="text-sm text-primary hover:text-primary"
>
View All
</button>
@ -251,11 +225,15 @@
<div
v-for="member in recentMembers"
:key="member._id"
class="flex items-center justify-between p-3 rounded-lg border border-gray-200"
class="flex items-center justify-between p-3 rounded-lg border border-default"
>
<div>
<p class="font-medium">{{ member.name }}</p>
<p class="text-sm text-gray-600">{{ member.email }}</p>
<p class="font-medium text-highlighted">
{{ member.name }}
</p>
<p class="text-sm text-muted">
{{ member.email }}
</p>
</div>
<div class="text-right">
<span
@ -264,25 +242,27 @@
>
{{ member.circle }}
</span>
<p class="text-xs text-gray-500">
<p class="text-xs text-dimmed">
{{ formatDate(member.createdAt) }}
</p>
</div>
</div>
</div>
<div v-else class="text-center py-6 text-gray-500">
<div v-else class="text-center py-6 text-dimmed">
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="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">Upcoming Events</h3>
<h3 class="text-lg font-semibold text-highlighted">
Upcoming Events
</h3>
<button
@click="navigateTo('/admin/events')"
class="text-sm text-primary-600 hover:text-primary-900"
class="text-sm text-primary hover:text-primary"
>
View All
</button>
@ -299,11 +279,13 @@
<div
v-for="event in upcomingEvents"
:key="event._id"
class="flex items-center justify-between p-3 rounded-lg border border-gray-200"
class="flex items-center justify-between p-3 rounded-lg border border-default"
>
<div>
<p class="font-medium">{{ event.title }}</p>
<p class="text-sm text-gray-600">
<p class="font-medium text-highlighted">
{{ event.title }}
</p>
<p class="text-sm text-muted">
{{ formatDateTime(event.startDate) }}
</p>
</div>
@ -314,13 +296,13 @@
>
{{ event.eventType }}
</span>
<p class="text-xs text-gray-500">
<p class="text-xs text-dimmed">
{{ event.location || "Online" }}
</p>
</div>
</div>
</div>
<div v-else class="text-center py-6 text-gray-500">
<div v-else class="text-center py-6 text-dimmed">
No upcoming events
</div>
</div>
@ -349,7 +331,7 @@ const getCircleBadgeClasses = (circle) => {
founder: "bg-purple-100 text-purple-800",
practitioner: "bg-green-100 text-green-800",
};
return classes[circle] || "bg-gray-100 text-gray-800";
return classes[circle] || "bg-accented text-default";
};
const getEventTypeBadgeClasses = (type) => {
@ -359,7 +341,7 @@ const getEventTypeBadgeClasses = (type) => {
social: "bg-purple-100 text-purple-800",
showcase: "bg-orange-100 text-orange-800",
};
return classes[type] || "bg-gray-100 text-gray-800";
return classes[type] || "bg-accented text-default";
};
const formatDate = (dateString) => {

View file

@ -1,10 +1,10 @@
<template>
<div>
<div class="bg-white border-b">
<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-gray-900">Member Management</h1>
<p class="text-gray-600">
<h1 class="text-2xl font-bold text-highlighted">Member Management</h1>
<p class="text-muted">
Manage Ghost Guild members, their contributions, and access levels
</p>
</div>
@ -18,11 +18,11 @@
<input
v-model="searchQuery"
placeholder="Search members..."
class="border border-gray-300 rounded-lg px-4 py-2 w-80 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="border border-default bg-elevated text-default placeholder-dimmed rounded-lg px-4 py-2 w-80 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<select
v-model="circleFilter"
class="border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="border border-default bg-elevated text-default rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">All Circles</option>
<option value="community">Community</option>
@ -39,8 +39,8 @@
</div>
<!-- Members Table -->
<div class="bg-white rounded-lg shadow overflow-hidden">
<div v-if="pending" class="p-8 text-center">
<div class="bg-elevated rounded-lg shadow overflow-hidden">
<div v-if="pending" class="p-8 text-center text-default">
<div class="inline-flex items-center">
<div
class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mr-3"
@ -54,58 +54,60 @@
</div>
<table v-else class="w-full">
<thead class="bg-gray-50">
<thead class="bg-muted">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
>
Name
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
>
Email
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
>
Circle
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
>
Contribution
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
>
Slack Status
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
>
Joined
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
>
Actions
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tbody class="bg-elevated divide-y divide-default">
<tr
v-for="member in filteredMembers"
:key="member._id"
class="hover:bg-gray-50"
class="hover:bg-muted"
>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">
<div class="text-sm font-medium text-highlighted">
{{ member.name }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-600">{{ member.email }}</div>
<div class="text-sm text-muted">
{{ member.email }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
@ -127,27 +129,27 @@
:class="
member.slackInvited
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
: 'bg-accented text-default'
"
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full"
>
{{ member.slackInvited ? "Invited" : "Pending" }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
<td class="px-6 py-4 whitespace-nowrap text-sm text-muted">
{{ formatDate(member.createdAt) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
<td class="px-6 py-4 whitespace-nowrap text-sm text-muted">
<div class="flex gap-2">
<button
@click="sendSlackInvite(member)"
class="text-primary-600 hover:text-primary-900"
class="text-primary hover:text-primary"
>
Slack Invite
</button>
<button
@click="editMember(member)"
class="text-primary-600 hover:text-primary-900"
class="text-primary hover:text-primary"
>
Edit
</button>
@ -159,7 +161,7 @@
<div
v-if="!pending && !error && filteredMembers.length === 0"
class="p-8 text-center text-gray-500"
class="p-8 text-center text-dimmed"
>
No members found matching your criteria
</div>
@ -171,26 +173,26 @@
v-if="showCreateModal"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
>
<div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
<div class="px-6 py-4 border-b">
<h3 class="text-lg font-semibold">Add New Member</h3>
<div class="bg-elevated rounded-lg shadow-xl max-w-md w-full mx-4">
<div class="px-6 py-4 border-b border-default">
<h3 class="text-lg font-semibold text-highlighted">Add New Member</h3>
</div>
<form @submit.prevent="createMember" class="p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1"
<label class="block text-sm font-medium text-default mb-1"
>Name</label
>
<input
v-model="newMember.name"
placeholder="Full name"
required
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="w-full border border-default bg-elevated text-default placeholder-dimmed 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"
<label class="block text-sm font-medium text-default mb-1"
>Email</label
>
<input
@ -198,45 +200,47 @@
type="email"
placeholder="email@example.com"
required
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="w-full border border-default bg-elevated text-default placeholder-dimmed 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"
<label class="block text-sm font-medium text-default mb-1"
>Circle</label
>
<select
<USelect
v-model="newMember.circle"
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</option>
<option value="founder">Founder</option>
<option value="practitioner">Practitioner</option>
</select>
:items="[
{ label: 'Community', value: 'community' },
{ label: 'Founder', value: 'founder' },
{ label: 'Practitioner', value: 'practitioner' },
]"
class="w-full"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1"
<label class="block text-sm font-medium text-default mb-1"
>Contribution Tier</label
>
<select
<USelect
v-model="newMember.contributionTier"
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="0">$0/month</option>
<option value="5">$5/month</option>
<option value="15">$15/month</option>
<option value="30">$30/month</option>
<option value="50">$50/month</option>
</select>
:items="[
{ label: '$0/month', value: '0' },
{ label: '$5/month', value: '5' },
{ label: '$15/month', value: '15' },
{ label: '$30/month', value: '30' },
{ label: '$50/month', value: '50' },
]"
class="w-full"
/>
</div>
<div class="flex justify-end gap-3 pt-4">
<button
type="button"
@click="showCreateModal = false"
class="px-4 py-2 text-gray-600 hover:text-gray-900"
class="px-4 py-2 text-muted hover:text-default"
>
Cancel
</button>

View file

@ -1,12 +1,10 @@
<template>
<div>
<div class="bg-white border-b">
<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-gray-900">Series Management</h1>
<p class="text-gray-600">
Manage event series and their relationships
</p>
<h1 class="text-2xl font-bold text-highlighted">Series Management</h1>
<p class="text-muted">Manage event series and their relationships</p>
</div>
</div>
</div>
@ -15,7 +13,7 @@
<!-- Series Overview -->
<div class="mb-8">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-white rounded-lg shadow p-6">
<div class="bg-elevated rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-3 bg-purple-100 rounded-full">
<Icon
@ -24,14 +22,14 @@
/>
</div>
<div class="ml-4">
<p class="text-sm text-gray-500">Active Series</p>
<p class="text-2xl font-semibold text-gray-900">
<p class="text-sm text-dimmed">Active Series</p>
<p class="text-2xl font-semibold text-highlighted">
{{ activeSeries.length }}
</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="bg-elevated rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-3 bg-blue-100 rounded-full">
<Icon
@ -40,14 +38,14 @@
/>
</div>
<div class="ml-4">
<p class="text-sm text-gray-500">Total Series Events</p>
<p class="text-2xl font-semibold text-gray-900">
<p class="text-sm text-dimmed">Total Series Events</p>
<p class="text-2xl font-semibold text-highlighted">
{{ totalSeriesEvents }}
</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="bg-elevated rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-3 bg-green-100 rounded-full">
<Icon
@ -56,8 +54,8 @@
/>
</div>
<div class="ml-4">
<p class="text-sm text-gray-500">Avg Events/Series</p>
<p class="text-2xl font-semibold text-gray-900">
<p class="text-sm text-dimmed">Avg Events/Series</p>
<p class="text-2xl font-semibold text-highlighted">
{{
activeSeries.length > 0
? Math.round(totalSeriesEvents / activeSeries.length)
@ -76,11 +74,11 @@
<input
v-model="searchQuery"
placeholder="Search series..."
class="border border-gray-300 rounded-lg px-4 py-2 w-80 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
class="border border-default bg-elevated text-default placeholder-dimmed rounded-lg px-4 py-2 w-80 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
<select
v-model="statusFilter"
class="border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
class="border border-default bg-elevated text-default rounded-lg px-4 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="">All Status</option>
<option value="active">Active</option>
@ -111,17 +109,17 @@
<div
class="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-500 mx-auto mb-4"
></div>
<p class="text-gray-600">Loading series...</p>
<p class="text-muted">Loading series...</p>
</div>
<div v-else-if="filteredSeries.length > 0" class="space-y-6">
<div
v-for="series in filteredSeries"
:key="series.id"
class="bg-white rounded-lg shadow overflow-hidden"
class="bg-elevated rounded-lg shadow overflow-hidden"
>
<!-- Series Header -->
<div class="px-6 py-4 bg-gray-50 border-b border-gray-200">
<div class="px-6 py-4 bg-muted border-b border-default">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div
@ -133,10 +131,10 @@
{{ formatSeriesType(series.type) }}
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900">
<h3 class="text-lg font-semibold text-highlighted">
{{ series.title }}
</h3>
<p class="text-sm text-gray-600">{{ series.description }}</p>
<p class="text-sm text-muted">{{ series.description }}</p>
</div>
</div>
<div class="flex items-center gap-3">
@ -147,12 +145,12 @@
? 'bg-green-100 text-green-700'
: series.status === 'upcoming'
? 'bg-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-700',
: 'bg-accented text-default',
]"
>
{{ series.status }}
</span>
<span class="text-sm text-gray-500">
<span class="text-sm text-dimmed">
{{ series.eventCount }} events
</span>
</div>
@ -160,11 +158,11 @@
</div>
<!-- Series Events -->
<div class="divide-y divide-gray-200">
<div class="divide-y divide-default">
<div
v-for="event in series.events"
:key="event.id"
class="px-6 py-4 hover:bg-gray-50"
class="px-6 py-4 hover:bg-muted"
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
@ -174,10 +172,10 @@
{{ event.series?.position || "?" }}
</div>
<div>
<h4 class="text-sm font-medium text-gray-900">
<h4 class="text-sm font-medium text-highlighted">
{{ event.title }}
</h4>
<p class="text-xs text-gray-500">
<p class="text-xs text-dimmed">
{{ formatEventDate(event.startDate) }}
</p>
</div>
@ -194,21 +192,21 @@
<div class="flex gap-1">
<NuxtLink
:to="`/events/${event.slug || event.id}`"
class="p-1 text-gray-400 hover:text-gray-600 rounded"
class="p-1 text-muted hover:text-default rounded"
title="View Event"
>
<Icon name="heroicons:eye" class="w-4 h-4" />
</NuxtLink>
<button
@click="editEvent(event)"
class="p-1 text-gray-400 hover:text-primary rounded"
class="p-1 text-muted hover:text-primary rounded"
title="Edit Event"
>
<Icon name="heroicons:pencil-square" class="w-4 h-4" />
</button>
<button
@click="removeFromSeries(event)"
class="p-1 text-gray-400 hover:text-red-600 rounded"
class="p-1 text-muted hover:text-red-600 rounded"
title="Remove from Series"
>
<Icon name="heroicons:x-mark" class="w-4 h-4" />
@ -220,27 +218,27 @@
</div>
<!-- Series Actions -->
<div class="px-6 py-3 bg-gray-50 border-t border-gray-200">
<div class="px-6 py-3 bg-muted border-t border-default">
<div class="flex justify-between items-center">
<div class="text-sm text-gray-500">
<div class="text-sm text-dimmed">
{{ formatDateRange(series.startDate, series.endDate) }}
</div>
<div class="flex gap-2">
<button
@click="editSeries(series)"
class="text-sm text-primary-600 hover:text-primary-700 font-medium"
class="text-sm text-primary hover:text-primary font-medium"
>
Edit Series
</button>
<button
@click="addEventToSeries(series)"
class="text-sm text-primary-600 hover:text-primary-700 font-medium"
class="text-sm text-primary hover:text-primary font-medium"
>
Add Event
</button>
<button
@click="duplicateSeries(series)"
class="text-sm text-primary-600 hover:text-primary-700 font-medium"
class="text-sm text-primary hover:text-primary font-medium"
>
Duplicate Series
</button>
@ -256,13 +254,13 @@
</div>
</div>
<div v-else class="text-center py-12 bg-white rounded-lg shadow">
<div v-else class="text-center py-12 bg-elevated rounded-lg shadow">
<Icon
name="heroicons:squares-2x2"
class="w-12 h-12 text-gray-400 mx-auto mb-3"
class="w-12 h-12 text-muted mx-auto mb-3"
/>
<p class="text-gray-600">No event series found</p>
<p class="text-sm text-gray-500 mt-2">
<p class="text-muted">No event series found</p>
<p class="text-sm text-dimmed mt-2">
Create events and group them into series to get started
</p>
</div>
@ -274,14 +272,14 @@
class="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center p-4 z-50"
>
<div
class="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
class="bg-elevated rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
>
<div class="px-6 py-4 border-b border-gray-200">
<div class="px-6 py-4 border-b border-default">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900">Edit Series</h3>
<h3 class="text-lg font-semibold text-highlighted">Edit Series</h3>
<button
@click="cancelEditSeries"
class="text-gray-400 hover:text-gray-600"
class="text-muted hover:text-default"
>
<Icon name="heroicons:x-mark" class="w-5 h-5" />
</button>
@ -290,62 +288,63 @@
<div class="p-6 space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"
<label class="block text-sm font-medium text-default mb-2"
>Series Title</label
>
<input
v-model="editingSeriesData.title"
type="text"
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
class="w-full border border-default bg-elevated text-default placeholder-dimmed rounded-lg px-4 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
placeholder="e.g., Co-op Game Dev Workshop Series"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"
<label class="block text-sm font-medium text-default mb-2"
>Description</label
>
<textarea
v-model="editingSeriesData.description"
rows="3"
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
class="w-full border border-default bg-elevated text-default placeholder-dimmed rounded-lg px-4 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
placeholder="Brief description of this series"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"
<label class="block text-sm font-medium text-default mb-2"
>Series Type</label
>
<select
<USelect
v-model="editingSeriesData.type"
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-purple-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>
</select>
:items="[
{ label: 'Workshop Series', value: 'workshop_series' },
{ label: 'Recurring Meetup', value: 'recurring_meetup' },
{ label: 'Multi-Day Event', value: 'multi_day' },
{ label: 'Course', value: 'course' },
]"
class="w-full"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"
<label class="block text-sm font-medium text-default mb-2"
>Total Events (optional)</label
>
<input
<UInput
v-model.number="editingSeriesData.totalEvents"
type="number"
min="1"
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
placeholder="Leave empty for ongoing series"
class="w-full"
/>
</div>
</div>
<div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
<div class="px-6 py-4 border-t border-default flex justify-end gap-3">
<button
@click="cancelEditSeries"
class="px-4 py-2 text-gray-600 hover:text-gray-700"
class="px-4 py-2 text-muted hover:text-default"
>
Cancel
</button>
@ -365,16 +364,16 @@
class="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center p-4 z-50"
>
<div
class="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
class="bg-elevated rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
>
<div class="px-6 py-4 border-b border-gray-200">
<div class="px-6 py-4 border-b border-default">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900">
<h3 class="text-lg font-semibold text-highlighted">
Bulk Series Operations
</h3>
<button
@click="showBulkModal = false"
class="text-gray-400 hover:text-gray-600"
class="text-muted hover:text-default"
>
<Icon name="heroicons:x-mark" class="w-5 h-5" />
</button>
@ -383,24 +382,24 @@
<div class="p-6 space-y-6">
<div>
<h4 class="text-sm font-medium text-gray-900 mb-3">
<h4 class="text-sm font-medium text-highlighted mb-3">
Series Management Tools
</h4>
<div class="space-y-3">
<button
@click="reorderAllSeries"
class="w-full text-left p-3 border border-gray-200 rounded-lg hover:bg-gray-50"
class="w-full text-left p-3 border border-default rounded-lg hover:bg-muted"
>
<div class="flex items-center">
<Icon
name="heroicons:arrows-up-down"
class="w-5 h-5 text-gray-400 mr-3"
class="w-5 h-5 text-muted mr-3"
/>
<div>
<p class="text-sm font-medium text-gray-900">
<p class="text-sm font-medium text-highlighted">
Auto-Reorder Series
</p>
<p class="text-xs text-gray-500">
<p class="text-xs text-dimmed">
Fix position numbers based on event dates
</p>
</div>
@ -409,18 +408,18 @@
<button
@click="validateAllSeries"
class="w-full text-left p-3 border border-gray-200 rounded-lg hover:bg-gray-50"
class="w-full text-left p-3 border border-default rounded-lg hover:bg-muted"
>
<div class="flex items-center">
<Icon
name="heroicons:check-circle"
class="w-5 h-5 text-gray-400 mr-3"
class="w-5 h-5 text-muted mr-3"
/>
<div>
<p class="text-sm font-medium text-gray-900">
<p class="text-sm font-medium text-highlighted">
Validate Series Data
</p>
<p class="text-xs text-gray-500">
<p class="text-xs text-dimmed">
Check for consistency issues
</p>
</div>
@ -429,18 +428,18 @@
<button
@click="exportSeriesData"
class="w-full text-left p-3 border border-gray-200 rounded-lg hover:bg-gray-50"
class="w-full text-left p-3 border border-default rounded-lg hover:bg-muted"
>
<div class="flex items-center">
<Icon
name="heroicons:document-arrow-down"
class="w-5 h-5 text-gray-400 mr-3"
class="w-5 h-5 text-muted mr-3"
/>
<div>
<p class="text-sm font-medium text-gray-900">
<p class="text-sm font-medium text-highlighted">
Export Series Data
</p>
<p class="text-xs text-gray-500">
<p class="text-xs text-dimmed">
Download series information as JSON
</p>
</div>
@ -450,10 +449,10 @@
</div>
</div>
<div class="px-6 py-4 border-t border-gray-200 flex justify-end">
<div class="px-6 py-4 border-t border-default flex justify-end">
<button
@click="showBulkModal = false"
class="px-4 py-2 text-gray-600 hover:text-gray-700"
class="px-4 py-2 text-muted hover:text-default"
>
Close
</button>

View file

@ -123,7 +123,6 @@
<div v-if="event.series?.isSeriesEvent" class="mb-8">
<EventSeriesBadge
:title="event.series.title"
:description="event.series.description"
:position="event.series.position"
:total-events="event.series.totalEvents"
:series-id="event.series.id"
@ -157,6 +156,22 @@
<h2 class="text-2xl font-bold text-ghost-100 mb-4">
About This Event
</h2>
<!-- Series Description -->
<div
v-if="event.series?.isSeriesEvent && event.series.description"
class="mb-6 p-4 bg-purple-500/5 rounded-lg border border-purple-500/20"
>
<h3
class="text-lg font-semibold text-purple-800 dark:text-purple-200 mb-2"
>
About the {{ event.series.title }} Series
</h3>
<p class="text-ghost-200">
{{ event.series.description }}
</p>
</div>
<p class="text-ghost-200">
{{ event.description }}
</p>

View file

@ -28,17 +28,19 @@
<div v-else>
<!-- Page Header -->
<PageHeader
:title="series.title"
:subtitle="series.description"
theme="purple"
size="large"
/>
<PageHeader :title="series.title" theme="purple" size="large" />
<!-- Series Meta -->
<section class="py-20 bg-[--ui-bg]">
<UContainer>
<div class="max-w-4xl mx-auto">
<!-- Series Description -->
<div v-if="series.description" class="mb-8">
<p class="text-lg text-[--ui-text-muted] leading-relaxed">
{{ series.description }}
</p>
</div>
<div class="flex items-center gap-4 mb-8 flex-wrap">
<span
:class="[

View file

@ -1,12 +1,12 @@
import Event from '../../models/event.js'
import { connectDB } from '../../utils/mongoose.js'
import jwt from 'jsonwebtoken'
import Event from "../../models/event.js";
import { connectDB } from "../../utils/mongoose.js";
import jwt from "jsonwebtoken";
export default defineEventHandler(async (event) => {
try {
// TODO: Temporarily disabled auth for testing - enable when authentication is set up
// const token = getCookie(event, 'auth-token') || getHeader(event, 'authorization')?.replace('Bearer ', '')
// if (!token) {
// throw createError({
// statusCode: 401,
@ -17,53 +17,60 @@ export default defineEventHandler(async (event) => {
// const config = useRuntimeConfig()
// const decoded = jwt.verify(token, config.jwtSecret)
const body = await readBody(event)
const body = await readBody(event);
// Validate required fields
if (!body.title || !body.description || !body.startDate || !body.endDate) {
throw createError({
statusCode: 400,
statusMessage: 'Missing required fields'
})
statusMessage: "Missing required fields",
});
}
await connectDB()
await connectDB();
const eventData = {
...body,
createdBy: 'admin@ghostguild.org', // TODO: Use actual authenticated user
createdBy: "admin@ghostguild.org", // TODO: Use actual authenticated user
startDate: new Date(body.startDate),
endDate: new Date(body.endDate),
registrationDeadline: body.registrationDeadline ? new Date(body.registrationDeadline) : null
}
registrationDeadline: body.registrationDeadline
? new Date(body.registrationDeadline)
: null,
};
// Ensure slug is not included in eventData (let the model generate it)
delete eventData.slug;
// Handle ticket data
if (body.tickets) {
eventData.tickets = {
enabled: body.tickets.enabled || false,
public: {
available: body.tickets.public?.available || false,
name: body.tickets.public?.name || 'Public Ticket',
description: body.tickets.public?.description || '',
name: body.tickets.public?.name || "Public Ticket",
description: body.tickets.public?.description || "",
price: body.tickets.public?.price || 0,
quantity: body.tickets.public?.quantity || null,
sold: 0, // Initialize sold count
earlyBirdPrice: body.tickets.public?.earlyBirdPrice || null,
earlyBirdDeadline: body.tickets.public?.earlyBirdDeadline ? new Date(body.tickets.public.earlyBirdDeadline) : null
}
}
earlyBirdDeadline: body.tickets.public?.earlyBirdDeadline
? new Date(body.tickets.public.earlyBirdDeadline)
: null,
},
};
}
const newEvent = new Event(eventData)
const savedEvent = await newEvent.save()
return savedEvent
const newEvent = new Event(eventData);
const savedEvent = await newEvent.save();
return savedEvent;
} catch (error) {
console.error('Error creating event:', error)
console.error("Error creating event:", error);
throw createError({
statusCode: 500,
statusMessage: error.message || 'Failed to create event'
})
statusMessage: error.message || "Failed to create event",
});
}
})
});

View file

@ -2,7 +2,7 @@ import mongoose from "mongoose";
const eventSchema = new mongoose.Schema({
title: { type: String, required: true },
slug: { type: String, required: true, unique: true },
slug: { type: String, unique: true }, // Auto-generated in pre-save hook
tagline: String,
description: { type: String, required: true },
content: String,
@ -133,7 +133,8 @@ function generateSlug(title) {
// Pre-save hook to generate slug
eventSchema.pre("save", async function (next) {
try {
if (this.isNew || this.isModified("title")) {
// Always generate slug if it doesn't exist or if title has changed
if (!this.slug || this.isNew || this.isModified("title")) {
let baseSlug = generateSlug(this.title);
let slug = baseSlug;
let counter = 1;