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
This commit is contained in:
parent
e8e3b84276
commit
896ad0336c
12 changed files with 915 additions and 360 deletions
|
|
@ -1,60 +1,76 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- Hero Section -->
|
||||
<div class="bg-gradient-to-br from-purple-600 via-blue-600 to-emerald-500 py-16">
|
||||
<UContainer>
|
||||
<div class="text-center">
|
||||
<h1 class="text-4xl md:text-6xl font-bold text-white mb-6">
|
||||
Event Series
|
||||
</h1>
|
||||
<p class="text-xl md:text-2xl text-purple-100 max-w-3xl mx-auto">
|
||||
Discover our multi-event series designed to take you on a journey of learning and growth
|
||||
</p>
|
||||
</div>
|
||||
</UContainer>
|
||||
</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 -->
|
||||
<div class="py-16 bg-gray-50">
|
||||
<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-purple-500 mx-auto mb-4"></div>
|
||||
<p class="text-gray-600">Loading series...</p>
|
||||
<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="space-y-8">
|
||||
<div
|
||||
v-for="series in filteredSeries"
|
||||
<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="bg-white rounded-xl shadow-lg overflow-hidden hover:shadow-xl transition-shadow duration-300"
|
||||
class="border border-[--ui-border] rounded overflow-hidden hover:border-primary transition-colors"
|
||||
>
|
||||
<!-- Series Header -->
|
||||
<div class="p-6 border-b border-gray-200">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<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-2">
|
||||
<div :class="[
|
||||
'inline-flex items-center px-3 py-1 rounded-full text-sm font-medium',
|
||||
getSeriesTypeBadgeClass(series.type)
|
||||
]">
|
||||
<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) }}
|
||||
</div>
|
||||
<span :class="[
|
||||
'inline-flex items-center px-2 py-1 rounded-full text-xs font-medium',
|
||||
series.status === 'active' ? 'bg-green-100 text-green-700' :
|
||||
series.status === 'upcoming' ? 'bg-blue-100 text-blue-700' :
|
||||
'bg-gray-100 text-gray-700'
|
||||
]">
|
||||
</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-gray-900 mb-2">{{ series.title }}</h2>
|
||||
<p class="text-gray-600 leading-relaxed">{{ series.description }}</p>
|
||||
<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">
|
||||
<div class="text-3xl font-bold text-purple-600 mb-1">{{ series.eventCount }}</div>
|
||||
<div class="text-sm text-gray-500">Events</div>
|
||||
<div v-if="series.totalEvents" class="text-xs text-gray-400 mt-1">
|
||||
<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>
|
||||
|
|
@ -62,47 +78,61 @@
|
|||
</div>
|
||||
|
||||
<!-- Events List -->
|
||||
<div class="divide-y divide-gray-100">
|
||||
<div
|
||||
v-for="event in series.events"
|
||||
<div class="divide-y divide-[--ui-border]">
|
||||
<div
|
||||
v-for="event in series.events"
|
||||
:key="event.id"
|
||||
class="p-4 hover:bg-gray-50 transition-colors duration-200"
|
||||
class="p-4 hover:bg-[--ui-bg-elevated] transition-colors"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4 flex-1">
|
||||
<div class="w-10 h-10 bg-purple-100 text-purple-600 rounded-full flex items-center justify-center text-sm font-semibold">
|
||||
{{ event.series?.position || '?' }}
|
||||
<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">
|
||||
<h3 class="font-medium text-gray-900 mb-1">{{ event.title }}</h3>
|
||||
<div class="flex items-center gap-4 text-sm text-gray-500">
|
||||
<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" />
|
||||
<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">
|
||||
<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">
|
||||
<span :class="[
|
||||
'inline-flex items-center px-2 py-1 rounded-full text-xs font-medium',
|
||||
getEventStatusClass(event)
|
||||
]">
|
||||
<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
|
||||
<NuxtLink
|
||||
:to="`/events/${event.slug || event.id}`"
|
||||
class="inline-flex items-center px-3 py-1 bg-purple-600 text-white text-sm rounded-lg hover:bg-purple-700 transition-colors"
|
||||
class="inline-flex items-center px-3 py-1 bg-primary text-white text-sm rounded hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
View Event
|
||||
View
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -110,125 +140,167 @@
|
|||
</div>
|
||||
|
||||
<!-- Series Footer -->
|
||||
<div v-if="series.startDate && series.endDate" class="px-6 py-4 bg-gray-50 border-t border-gray-200">
|
||||
<div class="flex items-center justify-between text-sm text-gray-500">
|
||||
<div class="flex items-center gap-1">
|
||||
<Icon name="heroicons:calendar-days" class="w-4 h-4" />
|
||||
Series runs from {{ 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
|
||||
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-gray-300 mx-auto mb-4" />
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-2">No Event Series Available</h3>
|
||||
<p class="text-gray-600 max-w-md mx-auto">
|
||||
We're currently planning exciting event series. Check back soon for multi-event learning journeys!
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// SEO
|
||||
useHead({
|
||||
title: 'Event Series - Ghost Guild',
|
||||
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.' }
|
||||
]
|
||||
})
|
||||
{
|
||||
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 }
|
||||
})
|
||||
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'
|
||||
)
|
||||
})
|
||||
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
|
||||
}
|
||||
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-100 text-emerald-700',
|
||||
'recurring_meetup': 'bg-blue-100 text-blue-700',
|
||||
'multi_day': 'bg-purple-100 text-purple-700',
|
||||
'course': 'bg-amber-100 text-amber-700',
|
||||
'tournament': 'bg-red-100 text-red-700'
|
||||
}
|
||||
return classes[type] || 'bg-gray-100 text-gray-700'
|
||||
}
|
||||
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', {
|
||||
weekday: 'long',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})
|
||||
}
|
||||
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
|
||||
})
|
||||
}
|
||||
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: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})
|
||||
|
||||
return `${formatter.format(start)} to ${formatter.format(end)}`
|
||||
}
|
||||
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 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 status = getEventStatus(event);
|
||||
const classes = {
|
||||
'Upcoming': 'bg-blue-100 text-blue-700',
|
||||
'Ongoing': 'bg-green-100 text-green-700',
|
||||
'Completed': 'bg-gray-100 text-gray-700'
|
||||
}
|
||||
return classes[status] || 'bg-gray-100 text-gray-700'
|
||||
}
|
||||
</script>
|
||||
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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue