ghostguild-org/app/pages/series/index.vue
Jennie Robinson Faber 896ad0336c Redesign interface across member dashboard and events pages
The changes involve a comprehensive interface redesign across multiple
pages, including:

- Updated peer support badge with shield design
- Switched privacy toggle to use USwitch component
- Added light/dark mode support throughout
- Enhanced layout and spacing in default template
- Added series details page with timeline view
- Improved event cards and status indicators
- Refreshed member profile styles for better readability
- Introduced global cursor styling for interactive elements
2025-10-09 16:25:57 +01:00

306 lines
11 KiB
Vue

<template>
<div>
<!-- Page Header -->
<PageHeader
title="Event Series"
subtitle="Discover our multi-event series designed to take you on a journey of learning and growth"
theme="purple"
size="large"
/>
<!-- Series Grid -->
<section class="py-20 bg-[--ui-bg]">
<UContainer>
<div v-if="pending" class="text-center py-12">
<div
class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"
></div>
<p class="text-[--ui-text-muted]">Loading series...</p>
</div>
<div
v-else-if="filteredSeries.length > 0"
class="max-w-4xl mx-auto space-y-6"
>
<div
v-for="series in filteredSeries"
:key="series.id"
class="border border-[--ui-border] rounded overflow-hidden hover:border-primary transition-colors"
>
<!-- Series Header -->
<div class="p-6 border-b border-[--ui-border]">
<div
class="flex flex-col md:flex-row md:items-start md:justify-between gap-4"
>
<div class="flex-1">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span
:class="[
'inline-flex items-center px-3 py-1 rounded text-sm font-medium',
getSeriesTypeBadgeClass(series.type),
]"
>
{{ formatSeriesType(series.type) }}
</span>
<span
:class="[
'inline-flex items-center px-2 py-1 rounded text-xs font-medium',
series.status === 'active'
? 'bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/30'
: series.status === 'upcoming'
? 'bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/30'
: 'bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30',
]"
>
{{ series.status }}
</span>
</div>
<h2 class="text-2xl font-bold text-[--ui-text] mb-2">
{{ series.title }}
</h2>
<p class="text-[--ui-text-muted] leading-relaxed">
{{ series.description }}
</p>
</div>
<div class="text-center md:text-right flex-shrink-0">
<div class="text-3xl font-bold text-[--ui-text] mb-1">
{{ series.eventCount }}
</div>
<div class="text-sm text-[--ui-text-muted]">Events</div>
<div
v-if="series.totalEvents"
class="text-xs text-[--ui-text-muted] mt-1"
>
of {{ series.totalEvents }} planned
</div>
</div>
</div>
</div>
<!-- Events List -->
<div class="divide-y divide-[--ui-border]">
<div
v-for="event in series.events"
:key="event.id"
class="p-4 hover:bg-[--ui-bg-elevated] transition-colors"
>
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-4 flex-1 min-w-0">
<div
class="w-8 h-8 bg-purple-500/10 text-purple-600 dark:text-purple-400 rounded-full flex items-center justify-center text-sm font-semibold flex-shrink-0 border border-purple-500/30"
>
{{ event.series?.position || "?" }}
</div>
<div class="flex-1 min-w-0">
<h3 class="font-medium text-[--ui-text] mb-1">
{{ event.title }}
</h3>
<div
class="flex items-center gap-4 text-sm text-[--ui-text-muted] flex-wrap"
>
<div class="flex items-center gap-1">
<Icon
name="heroicons:calendar-days"
class="w-4 h-4"
/>
{{ formatEventDate(event.startDate) }}
</div>
<div class="flex items-center gap-1">
<Icon name="heroicons:clock" class="w-4 h-4" />
{{ formatEventTime(event.startDate) }}
</div>
<div
v-if="event.registrations?.length"
class="flex items-center gap-1"
>
<Icon name="heroicons:users" class="w-4 h-4" />
{{ event.registrations.length }} registered
</div>
</div>
</div>
</div>
<div class="flex items-center gap-3 flex-shrink-0">
<span
:class="[
'inline-flex items-center px-2 py-1 rounded text-xs font-medium',
getEventStatusClass(event),
]"
>
{{ getEventStatus(event) }}
</span>
<NuxtLink
:to="`/events/${event.slug || event.id}`"
class="inline-flex items-center px-3 py-1 bg-primary text-white text-sm rounded hover:bg-primary/90 transition-colors"
>
View
</NuxtLink>
</div>
</div>
</div>
</div>
<!-- Series Footer -->
<div
class="px-6 py-4 bg-[--ui-bg-elevated] border-t border-[--ui-border]"
>
<div class="flex items-center justify-between gap-4">
<div
class="flex items-center gap-4 text-sm text-[--ui-text-muted] flex-wrap"
>
<div
v-if="series.startDate && series.endDate"
class="flex items-center gap-1"
>
<Icon name="heroicons:calendar-days" class="w-4 h-4" />
{{ formatDateRange(series.startDate, series.endDate) }}
</div>
<div
v-if="series.totalRegistrations"
class="flex items-center gap-1"
>
<Icon name="heroicons:users" class="w-4 h-4" />
{{ series.totalRegistrations }} total registrations
</div>
</div>
<NuxtLink
:to="`/series/${series.id}`"
class="inline-flex items-center px-4 py-2 bg-primary text-white text-sm font-medium rounded hover:bg-primary/90 transition-colors"
>
View Series
<Icon name="heroicons:arrow-right" class="w-4 h-4 ml-2" />
</NuxtLink>
</div>
</div>
</div>
</div>
<div v-else class="text-center py-16">
<Icon
name="heroicons:squares-2x2"
class="w-16 h-16 text-[--ui-text-muted] mx-auto mb-4 opacity-50"
/>
<h3 class="text-xl font-semibold text-[--ui-text] mb-2">
No Event Series Available
</h3>
<p class="text-[--ui-text-muted] max-w-md mx-auto">
We're currently planning exciting event series. Check back soon for
multi-event learning journeys!
</p>
</div>
</UContainer>
</section>
</div>
</template>
<script setup>
// SEO
useHead({
title: "Event Series - Ghost Guild",
meta: [
{
name: "description",
content:
"Discover our multi-event series designed to take you on a journey of learning and growth in cooperative game development and community building.",
},
],
});
// Fetch series data
const { data: seriesData, pending } = await useFetch("/api/series", {
query: { includeHidden: false },
});
// Filter for active and upcoming series only
const filteredSeries = computed(() => {
if (!seriesData.value) return [];
return seriesData.value.filter(
(series) => series.status === "active" || series.status === "upcoming",
);
});
// Helper functions
const formatSeriesType = (type) => {
const types = {
workshop_series: "Workshop Series",
recurring_meetup: "Recurring Meetup",
multi_day: "Multi-Day Event",
course: "Course",
tournament: "Tournament",
};
return types[type] || type;
};
const getSeriesTypeBadgeClass = (type) => {
const classes = {
workshop_series:
"bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30",
recurring_meetup:
"bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/30",
multi_day:
"bg-purple-500/10 text-purple-600 dark:text-purple-400 border border-purple-500/30",
course:
"bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/30",
tournament:
"bg-red-500/10 text-red-600 dark:text-red-400 border border-red-500/30",
};
return (
classes[type] ||
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30"
);
};
const formatEventDate = (date) => {
return new Date(date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
};
const formatEventTime = (date) => {
return new Date(date).toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
};
const formatDateRange = (startDate, endDate) => {
const start = new Date(startDate);
const end = new Date(endDate);
const formatter = new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
return `${formatter.format(start)} to ${formatter.format(end)}`;
};
const getEventStatus = (event) => {
const now = new Date();
const startDate = new Date(event.startDate);
const endDate = new Date(event.endDate);
if (now < startDate) return "Upcoming";
if (now >= startDate && now <= endDate) return "Ongoing";
return "Completed";
};
const getEventStatusClass = (event) => {
const status = getEventStatus(event);
const classes = {
Upcoming:
"bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/30",
Ongoing:
"bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/30",
Completed:
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30",
};
return (
classes[status] ||
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30"
);
};
</script>