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:
Jennie Robinson Faber 2025-10-09 16:25:57 +01:00
parent e8e3b84276
commit 896ad0336c
12 changed files with 915 additions and 360 deletions

View file

@ -6,18 +6,32 @@
:title="title"
>
<div
class="relative bg-gradient-to-br from-purple-500 to-purple-600 text-white px-3 py-2 rounded-lg shadow-lg border-2 border-purple-400/50 transform rotate-3 hover:rotate-0 transition-transform"
class="relative transform rotate-3 hover:rotate-0 transition-transform"
style="width: 60px; height: 66px"
>
<div class="flex flex-col items-center gap-0.5">
<Icon name="heroicons:chat-bubble-left-right-solid" class="w-4 h-4" />
<span
class="text-[10px] font-bold uppercase tracking-wide leading-tight"
>Peer<br />Support</span
>
<!-- Shield background -->
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1000 1000"
class="absolute inset-0 w-full h-full drop-shadow-lg"
>
<path
d="M500 70 150 175.3v217.1C150 785 500 930 500 930s350-145 350-537.6V175.2L500 70Z"
class="fill-purple-500"
/>
</svg>
<!-- Content on top of shield -->
<div class="absolute inset-0 flex flex-col items-center justify-center">
<Icon
name="heroicons:chat-bubble-left-right-solid"
class="w-6 h-6 text-white"
/>
</div>
<!-- Sparkle effect -->
<div
class="absolute -top-1 -right-1 w-2 h-2 bg-yellow-300 rounded-full animate-pulse"
class="absolute top-0 right-1 w-2 h-2 bg-yellow-300 rounded-full animate-pulse"
></div>
</div>
</div>

View file

@ -1,26 +1,36 @@
<template>
<div class="flex items-center gap-3 text-sm">
<span class="text-ghost-300 font-medium">{{ label }}:</span>
<UButtonGroup size="sm" class="privacy-toggle-group">
<UButton
:variant="modelValue === 'members' ? 'solid' : 'outline'"
:color="modelValue === 'members' ? 'blue' : 'neutral'"
@click="updateValue('members')"
class="privacy-toggle-btn"
:class="{ 'is-selected': modelValue === 'members' }"
<span class="text-gray-700 dark:text-ghost-400 text-xs font-medium"
>{{ label }}:</span
>
<div class="flex items-center gap-2">
<span
class="text-xs transition-colors"
:class="
isPrivate
? 'text-gray-500 dark:text-ghost-500'
: 'text-blue-600 dark:text-blue-400 font-semibold'
"
>
Members
</UButton>
<UButton
:variant="modelValue === 'private' ? 'solid' : 'outline'"
:color="modelValue === 'private' ? 'blue' : 'neutral'"
@click="updateValue('private')"
class="privacy-toggle-btn"
:class="{ 'is-selected': modelValue === 'private' }"
</span>
<USwitch
:model-value="isPrivate"
@update:model-value="togglePrivacy"
color="primary"
size="md"
/>
<span
class="text-xs transition-colors"
:class="
isPrivate
? 'text-blue-600 dark:text-blue-400 font-semibold'
: 'text-gray-500 dark:text-ghost-500'
"
>
Private
</UButton>
</UButtonGroup>
</span>
</div>
</div>
</template>
@ -38,33 +48,9 @@ const props = defineProps({
const emit = defineEmits(["update:modelValue"]);
const updateValue = (value) => {
emit("update:modelValue", value);
const isPrivate = computed(() => props.modelValue === "private");
const togglePrivacy = (value) => {
emit("update:modelValue", value ? "private" : "members");
};
</script>
<style scoped>
/* Unselected buttons - lighter background for visibility */
:deep(.privacy-toggle-btn:not(.is-selected)) {
background-color: rgb(68 64 60) !important; /* ghost-700 equivalent */
border-color: rgb(87 83 78) !important; /* ghost-600 equivalent */
color: rgb(214 211 209) !important; /* ghost-300 equivalent */
}
:deep(.privacy-toggle-btn:not(.is-selected):hover) {
background-color: rgb(87 83 78) !important; /* ghost-600 equivalent */
border-color: rgb(120 113 108) !important; /* ghost-500 equivalent */
}
/* Selected buttons - bright blue to stand out */
:deep(.privacy-toggle-btn.is-selected) {
background-color: rgb(59 130 246) !important; /* blue-500 */
border-color: rgb(59 130 246) !important; /* blue-500 */
color: white !important;
}
:deep(.privacy-toggle-btn.is-selected:hover) {
background-color: rgb(37 99 235) !important; /* blue-600 */
border-color: rgb(37 99 235) !important; /* blue-600 */
}
</style>

View file

@ -1,5 +1,5 @@
<template>
<div class="min-h-screen bg-ghost-900 flex relative">
<div class="min-h-screen bg-ghost-900 relative">
<!-- Background image at top - full page width -->
<div
class="absolute inset-x-0 pointer-events-none z-0"
@ -43,16 +43,19 @@
</div>
</div>
<!-- Main Content Column - Left -->
<div class="flex-1 overflow-y-auto relative z-[5]">
<div class="p-4 pt-20 md:p-8 md:pt-8 lg:p-16 max-w-4xl relative">
<slot />
<!-- Container to center content and sidebar together -->
<div class="lg:flex lg:justify-center lg:gap-0">
<!-- Main Content Column -->
<div class="lg:w-[800px] overflow-y-auto relative z-[5]">
<div class="relative">
<slot />
</div>
<AppFooter />
</div>
<AppFooter />
</div>
<!-- Desktop Navigation Column - Right -->
<AppNavigation class="hidden lg:block relative z-20" />
<!-- Desktop Navigation Column -->
<AppNavigation class="hidden lg:block relative z-20" />
</div>
<!-- Mobile Navigation Drawer -->
<USlideover v-model:open="isMobileMenuOpen" side="right">

View file

@ -64,7 +64,7 @@
<UContainer>
<div class="max-w-4xl mx-auto">
<!-- Event Meta Info -->
<div class="bg-ghost-800 rounded-xl p-6 mb-8 border border-ghost-700">
<div class="mb-8">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<p class="text-sm text-ghost-400">Date</p>
@ -118,20 +118,72 @@
</div>
</div>
<!-- Series Badge -->
<div v-if="event.series?.isSeriesEvent" class="mb-8">
<div
class="p-4 bg-gradient-to-r from-purple-500/10 to-blue-500/10 rounded-xl border border-purple-500/30"
>
<div class="flex items-start gap-3">
<div
class="flex-shrink-0 w-10 h-10 rounded-full bg-purple-500/20 flex items-center justify-center text-purple-600 dark:text-purple-400 font-bold"
>
{{ event.series.position }}
</div>
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<span
class="text-sm font-semibold text-purple-700 dark:text-purple-300"
>
Part of a Series
</span>
<span
v-if="event.series.totalEvents"
class="text-xs text-purple-600 dark:text-purple-400"
>
({{ event.series.position }} of
{{ event.series.totalEvents }})
</span>
</div>
<NuxtLink
:to="`/series/${event.series.id}`"
class="text-base font-medium text-purple-800 dark:text-purple-200 hover:underline"
>
{{ event.series.title }}
</NuxtLink>
<p
v-if="event.series.description"
class="text-sm text-purple-600 dark:text-purple-400 mt-1"
>
{{ event.series.description }}
</p>
</div>
<NuxtLink
:to="`/series/${event.series.id}`"
class="flex-shrink-0"
>
<UButton color="purple" variant="ghost" size="sm">
View Series
</UButton>
</NuxtLink>
</div>
</div>
</div>
<!-- Target Circles -->
<div
v-if="event.targetCircles && event.targetCircles.length > 0"
class="mb-8"
>
<div class="flex items-center space-x-2">
<span class="text-sm font-medium text-ghost-200"
<span
class="text-sm font-medium text-gray-800 dark:text-ghost-200"
>Recommended for:</span
>
<div class="flex flex-wrap gap-2">
<span
v-for="circle in event.targetCircles"
:key="circle"
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-900/30 text-blue-400"
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 border border-blue-300 dark:border-blue-800/50"
>
{{ formatCircleName(circle) }}
</span>
@ -198,28 +250,26 @@
</div>
<!-- Registration Section -->
<div
v-if="!event.isCancelled"
class="bg-ghost-800 rounded-xl p-8 border border-ghost-700"
>
<div v-if="!event.isCancelled">
<!-- Already Registered Status -->
<div v-if="registrationStatus === 'registered'">
<div
class="p-4 bg-green-900/20 rounded-lg border border-green-800 mb-6"
class="p-4 bg-green-100 dark:bg-green-900/20 rounded-lg border border-green-400 dark:border-green-800 mb-6"
>
<div class="flex items-start justify-between">
<div
class="flex flex-col md:flex-row md:items-start md:justify-between gap-4"
>
<div>
<p class="font-semibold text-green-300">
<p class="font-semibold text-green-800 dark:text-green-300">
You're registered!
</p>
<p class="text-sm text-green-400">
<p class="text-sm text-green-700 dark:text-green-400">
We've sent a confirmation to your email
</p>
</div>
<UButton
color="red"
variant="ghost"
size="sm"
color="error"
size="md"
@click="handleCancelRegistration"
:loading="isCancelling"
>
@ -361,7 +411,7 @@
</div>
<!-- Additional Information -->
<div class="mt-8 p-6 bg-ghost-800 rounded-xl border border-ghost-700">
<div class="mt-8 p-6 rounded-xl border border-ghost-700">
<h4 class="font-semibold text-ghost-100 mb-3">Questions?</h4>
<p class="text-sm text-ghost-200 mb-3">
If you have any questions about this event please drop us a line.

View file

@ -8,7 +8,7 @@
/>
<!-- Events Section with Tabs -->
<section class="py-20 bg-ghost-900 dark:bg-ghost-950">
<section class="bg-ghost-900 dark:bg-ghost-950">
<UContainer>
<UTabs
v-model="activeTab"
@ -138,10 +138,11 @@
v-if="activeSeries.length > 0"
class="space-y-6 max-w-6xl mx-auto mb-20"
>
<div
<NuxtLink
v-for="series in activeSeries.slice(0, 6)"
:key="series.id"
class="bg-ghost-900 rounded-xl p-6 shadow-lg border border-ghost-700"
:to="`/series/${series.id}`"
class="block bg-ghost-900 rounded-xl p-6 shadow-lg border border-ghost-700 hover:border-purple-500 hover:shadow-xl transition-all duration-300"
>
<div class="flex items-start justify-between mb-4">
<div
@ -158,7 +159,9 @@
</div>
</div>
<h3 class="text-lg font-semibold text-ghost-100 mb-2">
<h3
class="text-lg font-semibold text-ghost-100 mb-2 hover:text-purple-400 transition-colors"
>
{{ series.title }}
</h3>
@ -209,7 +212,7 @@
{{ series.status }}
</span>
</div>
</div>
</NuxtLink>
</div>
<!-- Attend Our Events -->

View file

@ -8,7 +8,7 @@
size="medium"
/>
<UContainer class="py-12">
<UContainer class="">
<!-- Loading State -->
<div
v-if="!memberData || authPending"
@ -17,7 +17,7 @@
<div class="text-center">
<div
class="w-8 h-8 border-4 border-whisper-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
></div>
/>
<p class="text-ghost-300">Loading your dashboard...</p>
</div>
</div>
@ -39,10 +39,7 @@
<h1 class="text-2xl font-bold text-ghost-100 ethereal-text">
Welcome to Ghost Guild, {{ memberData?.name }}!
</h1>
<p class="text-ghost-300 mt-2">
Your membership is active and you're part of our cooperative
community.
</p>
<p class="text-ghost-300 mt-2">Your membership is active!</p>
</div>
<div class="flex-shrink-0" v-if="memberData?.avatar">
<img
@ -63,14 +60,14 @@
<div class="flex flex-wrap gap-4 text-sm">
<div class="bg-ghost-800 border border-ghost-600 px-4 py-2">
<span class="text-ghost-400">Circle:</span>
<span class="font-medium text-whisper-300 ml-1 capitalize">{{
<span class="text-ghost-200">Circle:</span>
<span class="font-medium text-stone-50 ml-1 capitalize">{{
memberData?.circle
}}</span>
</div>
<div class="bg-ghost-800 border border-ghost-600 px-4 py-2">
<span class="text-ghost-400">Contribution:</span>
<span class="font-medium text-whisper-300 ml-1"
<span class="text-ghost-200">Contribution:</span>
<span class="font-medium text-stone-50 ml-1"
>${{ memberData?.contributionTier }} CAD/month</span
>
</div>
@ -93,21 +90,10 @@
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<UButton
disabled
to="/members?peerSupport=true"
variant="outline"
class="border-ghost-600 text-ghost-500 cursor-not-allowed justify-start"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 justify-start"
block
title="Coming soon"
>
Propose an Event
</UButton>
<UButton
disabled
variant="outline"
class="border-ghost-600 text-ghost-500 cursor-not-allowed justify-start"
block
title="Coming soon"
>
Book a Peer Session
</UButton>

View file

@ -17,7 +17,9 @@
<div
class="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
></div>
<p class="text-ghost-400">Loading your profile...</p>
<p class="text-gray-600 dark:text-ghost-400">
Loading your profile...
</p>
</div>
</div>
@ -32,7 +34,7 @@
<!-- Basic Information -->
<div>
<h2
class="text-2xl font-semibold mb-8 text-ghost-100 ethereal-text"
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-ghost-100 ethereal-text"
>
Basic Information
</h2>
@ -134,7 +136,7 @@
<!-- Professional Info -->
<div>
<h2
class="text-2xl font-semibold mb-8 text-ghost-100 ethereal-text"
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-ghost-100 ethereal-text"
>
Professional Information
</h2>
@ -203,7 +205,7 @@
<!-- Community Connections -->
<div>
<h2
class="text-2xl font-semibold mb-8 text-ghost-100 ethereal-text"
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-ghost-100 ethereal-text"
>
Community Connections
</h2>
@ -219,7 +221,7 @@
<!-- Tags input -->
<div>
<label
class="block text-sm font-medium text-ghost-200 mb-2"
class="block text-sm font-medium text-gray-800 dark:text-ghost-200 mb-2"
>
Skills & Topics
</label>
@ -237,7 +239,7 @@
<span
v-for="(tag, index) in formData.offering.tags"
:key="tag"
class="px-3 py-1 bg-blue-500/20 text-blue-300 rounded-full text-sm border border-blue-500/30 flex items-center gap-2 group hover:bg-blue-500/30 transition-colors cursor-pointer"
class="px-3 py-1 bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-300 rounded-full text-sm border border-blue-300 dark:border-blue-500/30 flex items-center gap-2 group hover:bg-blue-200 dark:hover:bg-blue-500/30 transition-colors cursor-pointer"
@click="removeOfferingTag(index)"
>
{{ tag }}
@ -251,7 +253,7 @@
<!-- Description textarea -->
<div>
<label
class="block text-sm font-medium text-ghost-200 mb-2"
class="block text-sm font-medium text-gray-800 dark:text-ghost-200 mb-2"
>
Details
</label>
@ -281,7 +283,7 @@
<!-- Tags input -->
<div>
<label
class="block text-sm font-medium text-ghost-200 mb-2"
class="block text-sm font-medium text-gray-800 dark:text-ghost-200 mb-2"
>
Skills & Topics
</label>
@ -299,7 +301,7 @@
<span
v-for="(tag, index) in formData.lookingFor.tags"
:key="tag"
class="px-3 py-1 bg-purple-500/20 text-purple-300 rounded-full text-sm border border-purple-500/30 flex items-center gap-2 group hover:bg-purple-500/30 transition-colors cursor-pointer"
class="px-3 py-1 bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 rounded-full text-sm border border-purple-300 dark:border-purple-500/30 flex items-center gap-2 group hover:bg-purple-200 dark:hover:bg-purple-500/30 transition-colors cursor-pointer"
@click="removeLookingForTag(index)"
>
{{ tag }}
@ -313,7 +315,7 @@
<!-- Description textarea -->
<div>
<label
class="block text-sm font-medium text-ghost-200 mb-2"
class="block text-sm font-medium text-gray-800 dark:text-ghost-200 mb-2"
>
Details
</label>
@ -338,7 +340,7 @@
<!-- Peer Support -->
<div>
<h2
class="text-2xl font-semibold mb-8 text-ghost-100 ethereal-text"
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-ghost-100 ethereal-text"
>
Peer Support
</h2>
@ -348,10 +350,14 @@
<div class="flex items-start gap-4">
<USwitch v-model="formData.peerSupportEnabled" />
<div>
<p class="font-medium text-ghost-200">
<p
class="font-medium text-gray-800 dark:text-ghost-200"
>
Offer Peer Support
</p>
<p class="text-sm text-ghost-400 mt-1">
<p
class="text-sm text-gray-600 dark:text-ghost-400 mt-1"
>
Make yourself available to support other members
</p>
</div>
@ -385,7 +391,7 @@
topic, index
) in formData.peerSupportSkillTopics"
:key="topic"
class="px-3 py-1 bg-blue-500/20 text-blue-300 rounded-full text-sm border border-blue-500/30 flex items-center gap-2 group hover:bg-blue-500/30 transition-colors cursor-pointer"
class="px-3 py-1 bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-300 rounded-full text-sm border border-blue-300 dark:border-blue-500/30 flex items-center gap-2 group hover:bg-blue-200 dark:hover:bg-blue-500/30 transition-colors cursor-pointer"
@click="removePeerSkillTopic(index)"
>
{{ topic }}
@ -454,7 +460,9 @@
class="w-full"
/>
<template #hint>
<span class="text-xs text-ghost-500">
<span
class="text-xs text-gray-500 dark:text-ghost-500"
>
{{ formData.peerSupportMessage?.length || 0 }}/200
characters
</span>
@ -481,7 +489,7 @@
<!-- Directory Settings -->
<div>
<h2
class="text-2xl font-semibold mb-8 text-ghost-100 ethereal-text"
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-ghost-100 ethereal-text"
>
Directory Visibility
</h2>
@ -489,10 +497,10 @@
<div class="flex items-start gap-4">
<USwitch v-model="formData.showInDirectory" />
<div>
<p class="font-medium text-ghost-200">
<p class="font-medium text-gray-800 dark:text-ghost-200">
Show in Member Directory
</p>
<p class="text-sm text-ghost-400 mt-1">
<p class="text-sm text-gray-600 dark:text-ghost-400 mt-1">
Allow other members to discover and connect with you
through the directory
</p>
@ -546,34 +554,42 @@
<!-- Current Membership -->
<div>
<h2
class="text-2xl font-semibold mb-6 text-ghost-100 ethereal-text"
class="text-2xl font-semibold mb-6 text-gray-900 dark:text-ghost-100 ethereal-text"
>
Current Membership
</h2>
<div
class="backdrop-blur-sm bg-ghost-800/50 border border-ghost-700 rounded-lg p-6 space-y-4"
class="backdrop-blur-sm bg-white/80 dark:bg-ghost-800/50 border border-gray-200 dark:border-ghost-700 rounded-lg p-6 space-y-4"
>
<div class="flex items-start justify-between">
<div>
<p class="text-sm text-ghost-400">Circle</p>
<p class="text-sm text-gray-600 dark:text-ghost-400">
Circle
</p>
<p
class="text-lg font-medium text-ghost-100 capitalize"
class="text-lg font-medium text-gray-900 dark:text-ghost-100 capitalize"
>
{{ memberData.circle }}
</p>
</div>
<div>
<p class="text-sm text-ghost-400">Contribution Level</p>
<p class="text-lg font-medium text-ghost-100">
<p class="text-sm text-gray-600 dark:text-ghost-400">
Contribution Level
</p>
<p
class="text-lg font-medium text-gray-900 dark:text-ghost-100"
>
${{ contributionTierDetails?.amount }}/month
</p>
</div>
</div>
<div v-if="memberData.subscriptionStartDate">
<p class="text-sm text-ghost-400">Member Since</p>
<p class="text-ghost-100">
<p class="text-sm text-gray-600 dark:text-ghost-400">
Member Since
</p>
<p class="text-gray-900 dark:text-ghost-100">
{{ formatDate(memberData.subscriptionStartDate) }}
</p>
</div>
@ -584,8 +600,10 @@
memberData.contributionTier !== '0'
"
>
<p class="text-sm text-ghost-400">Next Billing Date</p>
<p class="text-ghost-100">
<p class="text-sm text-gray-600 dark:text-ghost-400">
Next Billing Date
</p>
<p class="text-gray-900 dark:text-ghost-100">
{{ formatDate(memberData.nextBillingDate) }}
</p>
</div>
@ -595,15 +613,15 @@
<!-- Change Contribution Level -->
<div>
<h2
class="text-2xl font-semibold mb-6 text-ghost-100 ethereal-text"
class="text-2xl font-semibold mb-6 text-gray-900 dark:text-ghost-100 ethereal-text"
>
Change Contribution Level
</h2>
<div
class="backdrop-blur-sm bg-ghost-800/50 border border-ghost-700 rounded-lg p-6"
class="backdrop-blur-sm bg-white/80 dark:bg-ghost-800/50 border border-gray-200 dark:border-ghost-700 rounded-lg p-6"
>
<p class="text-ghost-300 mb-6">
<p class="text-gray-700 dark:text-ghost-300 mb-6">
Choose a new contribution level that works for you.
Changes will take effect on your next billing cycle.
</p>
@ -617,13 +635,15 @@
'w-full text-left p-4 rounded-lg border-2 transition-all',
selectedContributionTier === tier.value
? 'border-blue-400 bg-blue-500/20'
: 'border-ghost-600 bg-ghost-900/30 hover:border-ghost-500',
: 'border-gray-300 dark:border-ghost-600 bg-gray-50 dark:bg-ghost-900/30 hover:border-blue-300 dark:hover:border-ghost-500',
]"
@click="selectedContributionTier = tier.value"
>
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-ghost-100">
<p
class="font-medium text-gray-900 dark:text-ghost-100"
>
{{ tier.label }}
</p>
</div>
@ -669,20 +689,20 @@
<!-- Cancel Membership -->
<div>
<h2
class="text-2xl font-semibold mb-6 text-ghost-100 ethereal-text"
class="text-2xl font-semibold mb-6 text-gray-900 dark:text-ghost-100 ethereal-text"
>
Cancel Membership
</h2>
<div
class="backdrop-blur-sm bg-ghost-800/50 border border-ghost-700 rounded-lg p-6"
class="backdrop-blur-sm bg-white/80 dark:bg-ghost-800/50 border border-gray-200 dark:border-ghost-700 rounded-lg p-6"
>
<p class="text-ghost-300 mb-4">
<p class="text-gray-700 dark:text-ghost-300 mb-4">
We're sorry to see you go. If you cancel, you'll lose
access to member benefits at the end of your current
billing period.
</p>
<p class="text-sm text-ghost-400 mb-6">
<p class="text-sm text-gray-600 dark:text-ghost-400 mb-6">
Need a break? Consider switching to the free tier instead.
</p>
@ -1322,45 +1342,3 @@ useHead({
title: "Your Profile - Ghost Guild",
});
</script>
<style scoped>
/* Field labels - bright and readable */
:deep(label) {
color: rgb(231 229 228) !important; /* ghost-200 equivalent */
font-weight: 500;
text-align: left !important;
}
/* Field descriptions - lighter gray for readability */
:deep([class*="description"]) {
color: rgb(
168 162 158
) !important; /* ghost-400 equivalent - lighter than the dark background */
opacity: 0.9;
}
/* Full width inputs */
:deep(input),
:deep(textarea) {
width: 100% !important;
}
/* Input fields - respect light/dark mode */
:deep(input),
:deep(textarea) {
background-color: transparent !important;
color: var(--color-ghost-100) !important;
border-color: var(--color-ghost-600) !important;
}
:deep(input::placeholder),
:deep(textarea::placeholder) {
color: var(--color-ghost-500) !important;
}
:deep(input:focus),
:deep(textarea:focus) {
border-color: rgb(147 197 253) !important;
background-color: transparent !important;
}
</style>

View file

@ -7,7 +7,7 @@
size="medium"
/>
<section class="py-12 px-4">
<section class="">
<UContainer class="px-4">
<!-- Search and Filters -->
<div class="mb-8 space-y-4">
@ -56,8 +56,8 @@
class="px-3 py-1 rounded-full text-sm transition-all border"
:class="
selectedSkills.includes(skill)
? 'bg-purple-500/20 text-purple-300 border-purple-500/50'
: 'bg-ghost-800/50 text-ghost-400 border-ghost-700 hover:border-ghost-600'
? 'bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 border-purple-300 dark:border-purple-500/50'
: 'bg-gray-100 dark:bg-ghost-800/50 text-gray-700 dark:text-ghost-400 border-gray-300 dark:border-ghost-700 hover:border-gray-400 dark:hover:border-ghost-600'
"
@click="toggleSkill(skill)"
>
@ -94,8 +94,8 @@
class="px-3 py-1 rounded-full text-sm transition-all border"
:class="
selectedTopics.includes(topic)
? 'bg-purple-500/20 text-purple-300 border-purple-500/50'
: 'bg-ghost-800/50 text-ghost-400 border-ghost-700 hover:border-ghost-600'
? 'bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 border-purple-300 dark:border-purple-500/50'
: 'bg-gray-100 dark:bg-ghost-800/50 text-gray-700 dark:text-ghost-400 border-gray-300 dark:border-ghost-700 hover:border-gray-400 dark:hover:border-ghost-600'
"
@click="toggleTopic(topic)"
>
@ -129,7 +129,7 @@
<span class="text-ghost-400">Active filters:</span>
<span
v-if="selectedCircle && selectedCircle !== 'all'"
class="px-2 py-1 bg-purple-500/20 text-purple-300 rounded-full border border-purple-500/30 flex items-center gap-1"
class="px-2 py-1 bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 rounded-full border border-purple-300 dark:border-purple-500/30 flex items-center gap-1"
>
{{ circleLabels[selectedCircle] }}
<button
@ -142,7 +142,7 @@
</span>
<span
v-if="peerSupportFilter && peerSupportFilter !== 'all'"
class="px-2 py-1 bg-purple-500/20 text-purple-300 rounded-full border border-purple-500/30 flex items-center gap-1"
class="px-2 py-1 bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 rounded-full border border-purple-300 dark:border-purple-500/30 flex items-center gap-1"
>
Offering Peer Support
<button
@ -223,7 +223,7 @@
<div class="flex items-center gap-2 flex-wrap">
<span
class="px-2 py-0.5 bg-purple-500/20 text-purple-300 rounded text-xs border border-purple-500/30"
class="px-2 py-0.5 bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 rounded text-xs border border-purple-300 dark:border-purple-500/30"
>
{{ circleLabels[member.circle] }}
</span>
@ -270,7 +270,7 @@
<span
v-for="topic in member.peerSupport.topics"
:key="topic"
class="px-2 py-0.5 bg-purple-500/20 text-purple-200 rounded text-xs border border-purple-500/40"
class="px-2 py-0.5 bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-200 rounded text-xs border border-purple-300 dark:border-purple-500/40"
>
{{ topic }}
</span>
@ -301,7 +301,7 @@
<a
:href="`slack://user?team=T03A96LV4&id=${member.slackUserId}`"
@click.prevent="openSlackDM(member)"
class="inline-block px-3 py-1.5 bg-purple-500/20 text-purple-300 rounded border border-purple-500/30 hover:bg-purple-500/30 transition-colors text-sm font-medium cursor-pointer"
class="inline-block px-3 py-1.5 bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 rounded border border-purple-300 dark:border-purple-500/30 hover:bg-purple-200 dark:hover:bg-purple-500/30 transition-colors text-sm font-medium cursor-pointer"
>
Message {{ member.peerSupport.slackUsername }} on Slack
</a>
@ -339,7 +339,7 @@
<span
v-for="tag in member.offering.tags"
:key="tag"
class="px-2 py-0.5 bg-green-500/20 text-green-300 rounded text-xs border border-green-500/30"
class="px-2 py-0.5 bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300 rounded text-xs border border-green-300 dark:border-green-500/30"
>
{{ tag }}
</span>
@ -367,7 +367,7 @@
<span
v-for="tag in member.lookingFor.tags"
:key="tag"
class="px-2 py-0.5 bg-blue-500/20 text-blue-300 rounded text-xs border border-blue-500/30"
class="px-2 py-0.5 bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-300 rounded text-xs border border-blue-300 dark:border-blue-500/30"
>
{{ tag }}
</span>
@ -571,8 +571,14 @@ const openSlackDM = async (member) => {
window.open("https://gammaspace.slack.com", "_blank");
};
// Load on mount
// Load on mount and handle query params
onMounted(() => {
// Check for peerSupport query parameter
const route = useRoute();
if (route.query.peerSupport === "true") {
peerSupportFilter.value = "true";
}
loadMembers();
});

446
app/pages/series/[id].vue Normal file
View file

@ -0,0 +1,446 @@
<template>
<div>
<div v-if="pending" class="min-h-screen flex items-center justify-center">
<div class="text-center">
<div
class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"
></div>
<p class="text-[--ui-text-muted]">Loading series details...</p>
</div>
</div>
<div
v-else-if="error"
class="min-h-screen flex items-center justify-center"
>
<div class="text-center">
<h2 class="text-2xl font-bold text-[--ui-text] mb-2">
Series Not Found
</h2>
<p class="text-[--ui-text-muted] mb-6">
The event series you're looking for doesn't exist.
</p>
<NuxtLink to="/series" class="text-primary hover:underline">
Back to Event Series
</NuxtLink>
</div>
</div>
<div v-else>
<!-- Page Header -->
<PageHeader
:title="series.title"
:subtitle="series.description"
theme="purple"
size="large"
/>
<!-- Series Meta -->
<section class="py-20 bg-[--ui-bg]">
<UContainer>
<div class="max-w-4xl mx-auto">
<div class="flex items-center gap-4 mb-8 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-3 py-1 rounded text-sm font-medium',
getSeriesStatusClass(),
]"
>
{{ getSeriesStatusText() }}
</span>
</div>
<!-- Series Stats -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 mb-12">
<div>
<div class="text-3xl font-bold text-[--ui-text] mb-1">
{{ series.statistics.totalEvents }}
</div>
<div class="text-sm text-[--ui-text-muted]">Total Events</div>
</div>
<div>
<div class="text-3xl font-bold text-[--ui-text] mb-1">
{{ series.statistics.completedEvents }}
</div>
<div class="text-sm text-[--ui-text-muted]">Completed</div>
</div>
<div>
<div class="text-3xl font-bold text-[--ui-text] mb-1">
{{ series.statistics.upcomingEvents }}
</div>
<div class="text-sm text-[--ui-text-muted]">Upcoming</div>
</div>
<div v-if="series.statistics.totalRegistrations">
<div class="text-3xl font-bold text-[--ui-text] mb-1">
{{ series.statistics.totalRegistrations }}
</div>
<div class="text-sm text-[--ui-text-muted]">Registrations</div>
</div>
</div>
<!-- Series Date Range -->
<div
v-if="series.startDate && series.endDate"
class="flex items-center gap-2 text-[--ui-text-muted] mb-8"
>
<Icon name="heroicons:calendar-days" class="w-5 h-5" />
<span>
Series runs from
{{ formatDateRange(series.startDate, series.endDate) }}
</span>
</div>
<!-- Status Message -->
<div
v-if="series.statistics.isOngoing"
class="p-4 bg-green-500/10 border border-green-500/30 rounded mb-8"
>
<p class="text-green-600 dark:text-green-400 font-semibold mb-1">
This series is currently ongoing!
</p>
<p class="text-sm text-[--ui-text-muted]">
Register for upcoming events to join the learning journey.
</p>
</div>
<div
v-else-if="series.statistics.isUpcoming"
class="p-4 bg-blue-500/10 border border-blue-500/30 rounded mb-8"
>
<p class="text-blue-600 dark:text-blue-400 font-semibold mb-1">
This series is starting soon!
</p>
<p class="text-sm text-[--ui-text-muted]">
Mark your calendar and register for the events.
</p>
</div>
<div
v-else-if="series.statistics.isCompleted"
class="p-4 bg-gray-500/10 border border-gray-500/30 rounded mb-8"
>
<p class="text-[--ui-text] font-semibold mb-1">
This series has concluded.
</p>
<p class="text-sm text-[--ui-text-muted]">
Check out our other event series for more opportunities to learn
and connect.
</p>
</div>
</div>
</UContainer>
</section>
<!-- Events Timeline -->
<section class="py-20 bg-[--ui-bg-elevated]">
<UContainer>
<div class="max-w-4xl mx-auto">
<h2 class="text-2xl font-bold text-[--ui-text] mb-8">
Event Schedule
</h2>
<div class="space-y-4">
<div
v-for="(event, index) in series.events"
:key="event.id"
class="group"
>
<div class="flex items-start gap-4">
<!-- Position indicator -->
<div class="flex flex-col items-center flex-shrink-0">
<div
:class="[
'w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold border',
getEventTimelineColor(event),
]"
>
{{ event.series?.position || index + 1 }}
</div>
<div
v-if="index < series.events.length - 1"
class="w-0.5 h-12 bg-[--ui-border]"
></div>
</div>
<!-- Event Card -->
<NuxtLink
:to="`/events/${event.slug || event.id}`"
class="flex-1 border border-[--ui-border] rounded p-4 hover:border-primary transition-colors"
>
<div
class="flex flex-col md:flex-row md:items-start md:justify-between gap-3"
>
<!-- Event Info -->
<div class="flex-1 min-w-0">
<div class="flex items-start gap-2 mb-2 flex-wrap">
<h3
class="text-lg font-semibold text-[--ui-text] group-hover:text-primary transition-colors"
>
{{ event.title }}
</h3>
<span
:class="[
'inline-flex items-center px-2 py-0.5 rounded text-xs font-medium flex-shrink-0',
getEventStatusClass(event),
]"
>
{{ getEventStatus(event) }}
</span>
</div>
<p
v-if="event.description"
class="text-[--ui-text-muted] mb-3 line-clamp-2"
>
{{ event.description }}
</p>
<div
class="flex flex-wrap items-center gap-3 text-sm text-[--ui-text-muted]"
>
<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.location"
class="flex items-center gap-1"
>
<Icon name="heroicons:map-pin" class="w-4 h-4" />
{{ event.location }}
</div>
<div
v-if="event.registeredCount"
class="flex items-center gap-1"
>
<Icon name="heroicons:users" class="w-4 h-4" />
{{ event.registeredCount }} registered
</div>
</div>
</div>
<!-- Arrow -->
<div class="flex items-center md:pt-1">
<Icon
name="heroicons:arrow-right"
class="w-5 h-5 text-[--ui-text-muted] group-hover:text-primary group-hover:translate-x-1 transition-all"
/>
</div>
</div>
</NuxtLink>
</div>
</div>
</div>
</div>
</UContainer>
</section>
<!-- Questions -->
<section class="py-20 bg-[--ui-bg]">
<UContainer>
<div class="max-w-4xl mx-auto">
<h3 class="text-xl font-bold text-[--ui-text] mb-3">
Questions About This Series?
</h3>
<p class="text-[--ui-text-muted] mb-4">
If you have any questions about this event series, please reach
out to us.
</p>
<a
href="mailto:events@ghostguild.org"
class="text-primary hover:underline"
>
events@ghostguild.org
</a>
<div class="mt-8">
<NuxtLink to="/series" class="text-primary hover:underline">
Back to all event series
</NuxtLink>
</div>
</div>
</UContainer>
</section>
</div>
</div>
</template>
<script setup>
const route = useRoute();
// Fetch series data from API
const {
data: series,
pending,
error,
} = await useFetch(`/api/series/${route.params.id}`);
// Handle series not found
if (error.value?.statusCode === 404) {
throw createError({
statusCode: 404,
statusMessage: "Event series not found",
});
}
// 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 getSeriesStatusText = () => {
if (series.value.statistics.isOngoing) return "Ongoing";
if (series.value.statistics.isUpcoming) return "Starting Soon";
if (series.value.statistics.isCompleted) return "Completed";
return "Active";
};
const getSeriesStatusClass = () => {
if (series.value.statistics.isOngoing)
return "bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/30";
if (series.value.statistics.isUpcoming)
return "bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/30";
if (series.value.statistics.isCompleted)
return "bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30";
return "bg-purple-500/10 text-purple-600 dark:text-purple-400 border border-purple-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: "long",
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"
);
};
const getEventTimelineColor = (event) => {
const status = getEventStatus(event);
const classes = {
Upcoming:
"bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/30",
Ongoing:
"bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/30",
Completed:
"bg-purple-500/10 text-purple-600 dark:text-purple-400 border-purple-500/30",
};
return (
classes[status] ||
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border-gray-500/30"
);
};
// SEO Meta
useHead(() => ({
title: series.value
? `${series.value.title} - Event Series - Ghost Guild`
: "Event Series - Ghost Guild",
meta: [
{
name: "description",
content:
series.value?.description ||
"Explore our multi-event series designed for learning and growth",
},
],
}));
</script>
<style scoped>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View file

@ -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>