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> <template>
<div class="space-y-2"> <div class="space-y-2">
<div class="relative"> <div class="relative">
<input <UInput
v-model="naturalInput" v-model="naturalInput"
type="text"
:placeholder="placeholder" :placeholder="placeholder"
:class="[ :color="
'w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent', hasError && naturalInput.trim()
inputClass, ? 'error'
{ : isValidParse && naturalInput.trim()
'border-green-300 bg-green-50': isValidParse && naturalInput.trim(), ? 'success'
'border-red-300 bg-red-50': hasError && naturalInput.trim() : undefined
} "
]"
@input="parseNaturalInput" @input="parseNaturalInput"
@blur="onBlur" @blur="onBlur"
/> >
<div v-if="naturalInput.trim()" class="absolute right-3 top-2.5"> <template #trailing>
<Icon <Icon
v-if="isValidParse" v-if="isValidParse && naturalInput.trim()"
name="heroicons:check-circle" name="heroicons:check-circle"
class="w-5 h-5 text-green-500" class="w-5 h-5 text-green-500"
/> />
<Icon <Icon
v-else-if="hasError" v-else-if="hasError && naturalInput.trim()"
name="heroicons:exclamation-circle" name="heroicons:exclamation-circle"
class="w-5 h-5 text-red-500" class="w-5 h-5 text-red-500"
/> />
</div> </template>
</UInput>
</div> </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"> <div class="flex items-center gap-2">
<Icon name="heroicons:calendar" class="w-4 h-4" /> <Icon name="heroicons:calendar" class="w-4 h-4" />
<span>{{ formatParsedDate(parsedDate) }}</span> <span>{{ formatParsedDate(parsedDate) }}</span>
</div> </div>
</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"> <div class="flex items-center gap-2">
<Icon name="heroicons:exclamation-triangle" class="w-4 h-4" /> <Icon name="heroicons:exclamation-triangle" class="w-4 h-4" />
<span>{{ errorMessage }}</span> <span>{{ errorMessage }}</span>
</div> </div>
</div> </div>
<!-- Fallback datetime-local input --> <!-- Fallback datetime-local input -->
<details class="text-sm"> <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 Use traditional date picker
</summary> </summary>
<div class="mt-2"> <div class="mt-2">
<input <UInput
v-model="datetimeValue" v-model="datetimeValue"
type="datetime-local" 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" @change="onDatetimeChange"
/> />
</div> </div>
@ -65,174 +66,179 @@
</template> </template>
<script setup> <script setup>
import * as chrono from 'chrono-node' import * as chrono from "chrono-node";
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: String, type: String,
default: '' default: "",
}, },
placeholder: { placeholder: {
type: String, 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: { inputClass: {
type: String, type: String,
default: '' default: "",
}, },
required: { required: {
type: Boolean, type: Boolean,
default: false default: false,
} },
}) });
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(["update:modelValue"]);
const naturalInput = ref('') const naturalInput = ref("");
const parsedDate = ref(null) const parsedDate = ref(null);
const isValidParse = ref(false) const isValidParse = ref(false);
const hasError = ref(false) const hasError = ref(false);
const errorMessage = ref('') const errorMessage = ref("");
const datetimeValue = ref('') const datetimeValue = ref("");
// Initialize with current value // Initialize with current value
onMounted(() => { onMounted(() => {
if (props.modelValue) { if (props.modelValue) {
const date = new Date(props.modelValue) const date = new Date(props.modelValue);
if (!isNaN(date.getTime())) { if (!isNaN(date.getTime())) {
parsedDate.value = date parsedDate.value = date;
datetimeValue.value = formatForDatetimeLocal(date) datetimeValue.value = formatForDatetimeLocal(date);
isValidParse.value = true isValidParse.value = true;
} }
} }
}) });
// Watch for external changes to modelValue // Watch for external changes to modelValue
watch(() => props.modelValue, (newValue) => { watch(
if (newValue && newValue !== formatForDatetimeLocal(parsedDate.value)) { () => props.modelValue,
const date = new Date(newValue) (newValue) => {
if (!isNaN(date.getTime())) { if (newValue && newValue !== formatForDatetimeLocal(parsedDate.value)) {
parsedDate.value = date const date = new Date(newValue);
datetimeValue.value = formatForDatetimeLocal(date) if (!isNaN(date.getTime())) {
isValidParse.value = true parsedDate.value = date;
naturalInput.value = '' // Clear natural input when set externally 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 parseNaturalInput = () => {
const input = naturalInput.value.trim() const input = naturalInput.value.trim();
if (!input) { if (!input) {
reset() reset();
return return;
} }
try { try {
// Parse with chrono-node // Parse with chrono-node
const results = chrono.parse(input) const results = chrono.parse(input);
if (results.length > 0) { if (results.length > 0) {
const result = results[0] const result = results[0];
const date = result.date() const date = result.date();
// Validate the parsed date // Validate the parsed date
if (date && !isNaN(date.getTime())) { if (date && !isNaN(date.getTime())) {
parsedDate.value = date parsedDate.value = date;
isValidParse.value = true isValidParse.value = true;
hasError.value = false hasError.value = false;
datetimeValue.value = formatForDatetimeLocal(date) datetimeValue.value = formatForDatetimeLocal(date);
emit('update:modelValue', formatForDatetimeLocal(date)) emit("update:modelValue", formatForDatetimeLocal(date));
} else { } else {
setError('Could not parse this date format') setError("Could not parse this date format");
} }
} else { } 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) { } catch (error) {
setError('Error parsing date') setError("Error parsing date");
} }
} };
const onBlur = () => { const onBlur = () => {
// If we have a valid parse but the input changed, try to parse again // If we have a valid parse but the input changed, try to parse again
if (naturalInput.value.trim() && !isValidParse.value) { if (naturalInput.value.trim() && !isValidParse.value) {
parseNaturalInput() parseNaturalInput();
} }
} };
const onDatetimeChange = () => { const onDatetimeChange = () => {
if (datetimeValue.value) { if (datetimeValue.value) {
const date = new Date(datetimeValue.value) const date = new Date(datetimeValue.value);
if (!isNaN(date.getTime())) { if (!isNaN(date.getTime())) {
parsedDate.value = date parsedDate.value = date;
isValidParse.value = true isValidParse.value = true;
hasError.value = false hasError.value = false;
naturalInput.value = '' // Clear natural input when using traditional picker naturalInput.value = ""; // Clear natural input when using traditional picker
emit('update:modelValue', datetimeValue.value) emit("update:modelValue", datetimeValue.value);
} }
} else { } else {
reset() reset();
} }
} };
const reset = () => { const reset = () => {
parsedDate.value = null parsedDate.value = null;
isValidParse.value = false isValidParse.value = false;
hasError.value = false hasError.value = false;
errorMessage.value = '' errorMessage.value = "";
emit('update:modelValue', '') emit("update:modelValue", "");
} };
const setError = (message) => { const setError = (message) => {
isValidParse.value = false isValidParse.value = false;
hasError.value = true hasError.value = true;
errorMessage.value = message errorMessage.value = message;
parsedDate.value = null parsedDate.value = null;
} };
const formatForDatetimeLocal = (date) => { const formatForDatetimeLocal = (date) => {
if (!date) return '' if (!date) return "";
// Format as YYYY-MM-DDTHH:MM for datetime-local input // Format as YYYY-MM-DDTHH:MM for datetime-local input
const year = date.getFullYear() const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0') const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, '0') const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, '0') const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, '0') const minutes = String(date.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day}T${hours}:${minutes}` return `${year}-${month}-${day}T${hours}:${minutes}`;
} };
const formatParsedDate = (date) => { const formatParsedDate = (date) => {
if (!date) return '' if (!date) return "";
const now = new Date() const now = new Date();
const isToday = date.toDateString() === now.toDateString() const isToday = date.toDateString() === now.toDateString();
const tomorrow = new Date(now) const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1) tomorrow.setDate(tomorrow.getDate() + 1);
const isTomorrow = date.toDateString() === tomorrow.toDateString() const isTomorrow = date.toDateString() === tomorrow.toDateString();
const timeStr = date.toLocaleString('en-US', { const timeStr = date.toLocaleString("en-US", {
hour: 'numeric', hour: "numeric",
minute: '2-digit', minute: "2-digit",
hour12: true hour12: true,
}) });
if (isToday) { if (isToday) {
return `Today at ${timeStr}` return `Today at ${timeStr}`;
} else if (isTomorrow) { } else if (isTomorrow) {
return `Tomorrow at ${timeStr}` return `Tomorrow at ${timeStr}`;
} else { } else {
return date.toLocaleString('en-US', { return date.toLocaleString("en-US", {
weekday: 'long', weekday: "long",
year: 'numeric', year: "numeric",
month: 'long', month: "long",
day: 'numeric', day: "numeric",
hour: 'numeric', hour: "numeric",
minute: '2-digit', minute: "2-digit",
hour12: true hour12: true,
}) });
} }
} };
</script> </script>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,10 +1,10 @@
<template> <template>
<div> <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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="py-6"> <div class="py-6">
<h1 class="text-2xl font-bold text-gray-900">Admin Dashboard</h1> <h1 class="text-2xl font-bold text-highlighted">Admin Dashboard</h1>
<p class="text-gray-600"> <p class="text-muted">
Manage Ghost Guild members, events, and community operations Manage Ghost Guild members, events, and community operations
</p> </p>
</div> </div>
@ -14,10 +14,10 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Quick Stats --> <!-- Quick Stats -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8"> <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 class="flex items-center justify-between">
<div> <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"> <p class="text-2xl font-bold text-blue-600">
{{ stats.totalMembers || 0 }} {{ stats.totalMembers || 0 }}
</p> </p>
@ -42,10 +42,10 @@
</div> </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 justify-between"> <div class="flex items-center justify-between">
<div> <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"> <p class="text-2xl font-bold text-green-600">
{{ stats.activeEvents || 0 }} {{ stats.activeEvents || 0 }}
</p> </p>
@ -70,10 +70,10 @@
</div> </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 justify-between"> <div class="flex items-center justify-between">
<div> <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"> <p class="text-2xl font-bold text-purple-600">
${{ stats.monthlyRevenue || 0 }} ${{ stats.monthlyRevenue || 0 }}
</p> </p>
@ -98,10 +98,10 @@
</div> </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 justify-between"> <div class="flex items-center justify-between">
<div> <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"> <p class="text-2xl font-bold text-orange-600">
{{ stats.pendingSlackInvites || 0 }} {{ stats.pendingSlackInvites || 0 }}
</p> </p>
@ -128,8 +128,8 @@
</div> </div>
<!-- Quick Actions --> <!-- Quick Actions -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div class="bg-white rounded-lg shadow p-6"> <div class="bg-elevated rounded-lg shadow p-6">
<div class="text-center"> <div class="text-center">
<div <div
class="w-16 h-16 bg-blue-100 rounded-2xl flex items-center justify-center mx-auto mb-4" class="w-16 h-16 bg-blue-100 rounded-2xl flex items-center justify-center mx-auto mb-4"
@ -148,8 +148,10 @@
></path> ></path>
</svg> </svg>
</div> </div>
<h3 class="text-lg font-semibold mb-2">Add New Member</h3> <h3 class="text-lg font-semibold mb-2 text-highlighted">
<p class="text-gray-600 text-sm mb-4"> Add New Member
</h3>
<p class="text-muted text-sm mb-4">
Add a new member to the Ghost Guild community Add a new member to the Ghost Guild community
</p> </p>
<button <button
@ -161,7 +163,7 @@
</div> </div>
</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="text-center">
<div <div
class="w-16 h-16 bg-green-100 rounded-2xl flex items-center justify-center mx-auto mb-4" class="w-16 h-16 bg-green-100 rounded-2xl flex items-center justify-center mx-auto mb-4"
@ -180,8 +182,10 @@
></path> ></path>
</svg> </svg>
</div> </div>
<h3 class="text-lg font-semibold mb-2">Create Event</h3> <h3 class="text-lg font-semibold mb-2 text-highlighted">
<p class="text-gray-600 text-sm mb-4"> Create Event
</h3>
<p class="text-muted text-sm mb-4">
Schedule a new community event or workshop Schedule a new community event or workshop
</p> </p>
<button <button
@ -192,49 +196,19 @@
</button> </button>
</div> </div>
</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> </div>
<!-- Recent Activity --> <!-- Recent Activity -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-white rounded-lg shadow"> <div class="bg-elevated rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200"> <div class="px-6 py-4 border-b border-default">
<div class="flex justify-between items-center"> <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 <button
@click="navigateTo('/admin/members')" @click="navigateTo('/admin/members')"
class="text-sm text-primary-600 hover:text-primary-900" class="text-sm text-primary hover:text-primary"
> >
View All View All
</button> </button>
@ -251,11 +225,15 @@
<div <div
v-for="member in recentMembers" v-for="member in recentMembers"
:key="member._id" :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> <div>
<p class="font-medium">{{ member.name }}</p> <p class="font-medium text-highlighted">
<p class="text-sm text-gray-600">{{ member.email }}</p> {{ member.name }}
</p>
<p class="text-sm text-muted">
{{ member.email }}
</p>
</div> </div>
<div class="text-right"> <div class="text-right">
<span <span
@ -264,25 +242,27 @@
> >
{{ member.circle }} {{ member.circle }}
</span> </span>
<p class="text-xs text-gray-500"> <p class="text-xs text-dimmed">
{{ formatDate(member.createdAt) }} {{ formatDate(member.createdAt) }}
</p> </p>
</div> </div>
</div> </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 No recent members
</div> </div>
</div> </div>
</div> </div>
<div class="bg-white rounded-lg shadow"> <div class="bg-elevated rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200"> <div class="px-6 py-4 border-b border-default">
<div class="flex justify-between items-center"> <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 <button
@click="navigateTo('/admin/events')" @click="navigateTo('/admin/events')"
class="text-sm text-primary-600 hover:text-primary-900" class="text-sm text-primary hover:text-primary"
> >
View All View All
</button> </button>
@ -299,11 +279,13 @@
<div <div
v-for="event in upcomingEvents" v-for="event in upcomingEvents"
:key="event._id" :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> <div>
<p class="font-medium">{{ event.title }}</p> <p class="font-medium text-highlighted">
<p class="text-sm text-gray-600"> {{ event.title }}
</p>
<p class="text-sm text-muted">
{{ formatDateTime(event.startDate) }} {{ formatDateTime(event.startDate) }}
</p> </p>
</div> </div>
@ -314,13 +296,13 @@
> >
{{ event.eventType }} {{ event.eventType }}
</span> </span>
<p class="text-xs text-gray-500"> <p class="text-xs text-dimmed">
{{ event.location || "Online" }} {{ event.location || "Online" }}
</p> </p>
</div> </div>
</div> </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 No upcoming events
</div> </div>
</div> </div>
@ -349,7 +331,7 @@ const getCircleBadgeClasses = (circle) => {
founder: "bg-purple-100 text-purple-800", founder: "bg-purple-100 text-purple-800",
practitioner: "bg-green-100 text-green-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) => { const getEventTypeBadgeClasses = (type) => {
@ -359,7 +341,7 @@ const getEventTypeBadgeClasses = (type) => {
social: "bg-purple-100 text-purple-800", social: "bg-purple-100 text-purple-800",
showcase: "bg-orange-100 text-orange-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) => { const formatDate = (dateString) => {

View file

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

View file

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

View file

@ -123,7 +123,6 @@
<div v-if="event.series?.isSeriesEvent" class="mb-8"> <div v-if="event.series?.isSeriesEvent" class="mb-8">
<EventSeriesBadge <EventSeriesBadge
:title="event.series.title" :title="event.series.title"
:description="event.series.description"
:position="event.series.position" :position="event.series.position"
:total-events="event.series.totalEvents" :total-events="event.series.totalEvents"
:series-id="event.series.id" :series-id="event.series.id"
@ -157,6 +156,22 @@
<h2 class="text-2xl font-bold text-ghost-100 mb-4"> <h2 class="text-2xl font-bold text-ghost-100 mb-4">
About This Event About This Event
</h2> </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"> <p class="text-ghost-200">
{{ event.description }} {{ event.description }}
</p> </p>

View file

@ -28,17 +28,19 @@
<div v-else> <div v-else>
<!-- Page Header --> <!-- Page Header -->
<PageHeader <PageHeader :title="series.title" theme="purple" size="large" />
:title="series.title"
:subtitle="series.description"
theme="purple"
size="large"
/>
<!-- Series Meta --> <!-- Series Meta -->
<section class="py-20 bg-[--ui-bg]"> <section class="py-20 bg-[--ui-bg]">
<UContainer> <UContainer>
<div class="max-w-4xl mx-auto"> <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"> <div class="flex items-center gap-4 mb-8 flex-wrap">
<span <span
:class="[ :class="[

View file

@ -1,12 +1,12 @@
import Event from '../../models/event.js' import Event from "../../models/event.js";
import { connectDB } from '../../utils/mongoose.js' import { connectDB } from "../../utils/mongoose.js";
import jwt from 'jsonwebtoken' import jwt from "jsonwebtoken";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
try { try {
// TODO: Temporarily disabled auth for testing - enable when authentication is set up // TODO: Temporarily disabled auth for testing - enable when authentication is set up
// const token = getCookie(event, 'auth-token') || getHeader(event, 'authorization')?.replace('Bearer ', '') // const token = getCookie(event, 'auth-token') || getHeader(event, 'authorization')?.replace('Bearer ', '')
// if (!token) { // if (!token) {
// throw createError({ // throw createError({
// statusCode: 401, // statusCode: 401,
@ -17,53 +17,60 @@ export default defineEventHandler(async (event) => {
// const config = useRuntimeConfig() // const config = useRuntimeConfig()
// const decoded = jwt.verify(token, config.jwtSecret) // const decoded = jwt.verify(token, config.jwtSecret)
const body = await readBody(event) const body = await readBody(event);
// Validate required fields // Validate required fields
if (!body.title || !body.description || !body.startDate || !body.endDate) { if (!body.title || !body.description || !body.startDate || !body.endDate) {
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
statusMessage: 'Missing required fields' statusMessage: "Missing required fields",
}) });
} }
await connectDB() await connectDB();
const eventData = { const eventData = {
...body, ...body,
createdBy: 'admin@ghostguild.org', // TODO: Use actual authenticated user createdBy: "admin@ghostguild.org", // TODO: Use actual authenticated user
startDate: new Date(body.startDate), startDate: new Date(body.startDate),
endDate: new Date(body.endDate), 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 // Handle ticket data
if (body.tickets) { if (body.tickets) {
eventData.tickets = { eventData.tickets = {
enabled: body.tickets.enabled || false, enabled: body.tickets.enabled || false,
public: { public: {
available: body.tickets.public?.available || false, available: body.tickets.public?.available || false,
name: body.tickets.public?.name || 'Public Ticket', name: body.tickets.public?.name || "Public Ticket",
description: body.tickets.public?.description || '', description: body.tickets.public?.description || "",
price: body.tickets.public?.price || 0, price: body.tickets.public?.price || 0,
quantity: body.tickets.public?.quantity || null, quantity: body.tickets.public?.quantity || null,
sold: 0, // Initialize sold count sold: 0, // Initialize sold count
earlyBirdPrice: body.tickets.public?.earlyBirdPrice || null, 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() const newEvent = new Event(eventData);
return savedEvent const savedEvent = await newEvent.save();
return savedEvent;
} catch (error) { } catch (error) {
console.error('Error creating event:', error) console.error("Error creating event:", error);
throw createError({ throw createError({
statusCode: 500, 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({ const eventSchema = new mongoose.Schema({
title: { type: String, required: true }, 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, tagline: String,
description: { type: String, required: true }, description: { type: String, required: true },
content: String, content: String,
@ -133,7 +133,8 @@ function generateSlug(title) {
// Pre-save hook to generate slug // Pre-save hook to generate slug
eventSchema.pre("save", async function (next) { eventSchema.pre("save", async function (next) {
try { 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 baseSlug = generateSlug(this.title);
let slug = baseSlug; let slug = baseSlug;
let counter = 1; let counter = 1;