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" :title="title"
> >
<div <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"> <!-- Shield background -->
<Icon name="heroicons:chat-bubble-left-right-solid" class="w-4 h-4" /> <svg
<span xmlns="http://www.w3.org/2000/svg"
class="text-[10px] font-bold uppercase tracking-wide leading-tight" viewBox="0 0 1000 1000"
>Peer<br />Support</span 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> </div>
<!-- Sparkle effect --> <!-- Sparkle effect -->
<div <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> </div>
</div> </div>

View file

@ -1,26 +1,36 @@
<template> <template>
<div class="flex items-center gap-3 text-sm"> <div class="flex items-center gap-3 text-sm">
<span class="text-ghost-300 font-medium">{{ label }}:</span> <span class="text-gray-700 dark:text-ghost-400 text-xs font-medium"
<UButtonGroup size="sm" class="privacy-toggle-group"> >{{ label }}:</span
<UButton >
:variant="modelValue === 'members' ? 'solid' : 'outline'" <div class="flex items-center gap-2">
:color="modelValue === 'members' ? 'blue' : 'neutral'" <span
@click="updateValue('members')" class="text-xs transition-colors"
class="privacy-toggle-btn" :class="
:class="{ 'is-selected': modelValue === 'members' }" isPrivate
? 'text-gray-500 dark:text-ghost-500'
: 'text-blue-600 dark:text-blue-400 font-semibold'
"
> >
Members Members
</UButton> </span>
<UButton <USwitch
:variant="modelValue === 'private' ? 'solid' : 'outline'" :model-value="isPrivate"
:color="modelValue === 'private' ? 'blue' : 'neutral'" @update:model-value="togglePrivacy"
@click="updateValue('private')" color="primary"
class="privacy-toggle-btn" size="md"
:class="{ 'is-selected': modelValue === 'private' }" />
<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 Private
</UButton> </span>
</UButtonGroup> </div>
</div> </div>
</template> </template>
@ -38,33 +48,9 @@ const props = defineProps({
const emit = defineEmits(["update:modelValue"]); const emit = defineEmits(["update:modelValue"]);
const updateValue = (value) => { const isPrivate = computed(() => props.modelValue === "private");
emit("update:modelValue", value);
const togglePrivacy = (value) => {
emit("update:modelValue", value ? "private" : "members");
}; };
</script> </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> <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 --> <!-- Background image at top - full page width -->
<div <div
class="absolute inset-x-0 pointer-events-none z-0" class="absolute inset-x-0 pointer-events-none z-0"
@ -43,16 +43,19 @@
</div> </div>
</div> </div>
<!-- Main Content Column - Left --> <!-- Container to center content and sidebar together -->
<div class="flex-1 overflow-y-auto relative z-[5]"> <div class="lg:flex lg:justify-center lg:gap-0">
<div class="p-4 pt-20 md:p-8 md:pt-8 lg:p-16 max-w-4xl relative"> <!-- Main Content Column -->
<slot /> <div class="lg:w-[800px] overflow-y-auto relative z-[5]">
<div class="relative">
<slot />
</div>
<AppFooter />
</div> </div>
<AppFooter />
</div>
<!-- Desktop Navigation Column - Right --> <!-- Desktop Navigation Column -->
<AppNavigation class="hidden lg:block relative z-20" /> <AppNavigation class="hidden lg:block relative z-20" />
</div>
<!-- Mobile Navigation Drawer --> <!-- Mobile Navigation Drawer -->
<USlideover v-model:open="isMobileMenuOpen" side="right"> <USlideover v-model:open="isMobileMenuOpen" side="right">

View file

@ -64,7 +64,7 @@
<UContainer> <UContainer>
<div class="max-w-4xl mx-auto"> <div class="max-w-4xl mx-auto">
<!-- Event Meta Info --> <!-- 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 class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div> <div>
<p class="text-sm text-ghost-400">Date</p> <p class="text-sm text-ghost-400">Date</p>
@ -118,20 +118,72 @@
</div> </div>
</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 --> <!-- Target Circles -->
<div <div
v-if="event.targetCircles && event.targetCircles.length > 0" v-if="event.targetCircles && event.targetCircles.length > 0"
class="mb-8" class="mb-8"
> >
<div class="flex items-center space-x-2"> <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 >Recommended for:</span
> >
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<span <span
v-for="circle in event.targetCircles" v-for="circle in event.targetCircles"
:key="circle" :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) }} {{ formatCircleName(circle) }}
</span> </span>
@ -198,28 +250,26 @@
</div> </div>
<!-- Registration Section --> <!-- Registration Section -->
<div <div v-if="!event.isCancelled">
v-if="!event.isCancelled"
class="bg-ghost-800 rounded-xl p-8 border border-ghost-700"
>
<!-- Already Registered Status --> <!-- Already Registered Status -->
<div v-if="registrationStatus === 'registered'"> <div v-if="registrationStatus === 'registered'">
<div <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> <div>
<p class="font-semibold text-green-300"> <p class="font-semibold text-green-800 dark:text-green-300">
You're registered! You're registered!
</p> </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 We've sent a confirmation to your email
</p> </p>
</div> </div>
<UButton <UButton
color="red" color="error"
variant="ghost" size="md"
size="sm"
@click="handleCancelRegistration" @click="handleCancelRegistration"
:loading="isCancelling" :loading="isCancelling"
> >
@ -361,7 +411,7 @@
</div> </div>
<!-- Additional Information --> <!-- 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> <h4 class="font-semibold text-ghost-100 mb-3">Questions?</h4>
<p class="text-sm text-ghost-200 mb-3"> <p class="text-sm text-ghost-200 mb-3">
If you have any questions about this event please drop us a line. If you have any questions about this event please drop us a line.

View file

@ -8,7 +8,7 @@
/> />
<!-- Events Section with Tabs --> <!-- 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> <UContainer>
<UTabs <UTabs
v-model="activeTab" v-model="activeTab"
@ -138,10 +138,11 @@
v-if="activeSeries.length > 0" v-if="activeSeries.length > 0"
class="space-y-6 max-w-6xl mx-auto mb-20" class="space-y-6 max-w-6xl mx-auto mb-20"
> >
<div <NuxtLink
v-for="series in activeSeries.slice(0, 6)" v-for="series in activeSeries.slice(0, 6)"
:key="series.id" :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 class="flex items-start justify-between mb-4">
<div <div
@ -158,7 +159,9 @@
</div> </div>
</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 }} {{ series.title }}
</h3> </h3>
@ -209,7 +212,7 @@
{{ series.status }} {{ series.status }}
</span> </span>
</div> </div>
</div> </NuxtLink>
</div> </div>
<!-- Attend Our Events --> <!-- Attend Our Events -->

View file

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

View file

@ -17,7 +17,9 @@
<div <div
class="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" class="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
></div> ></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>
</div> </div>
@ -32,7 +34,7 @@
<!-- Basic Information --> <!-- Basic Information -->
<div> <div>
<h2 <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 Basic Information
</h2> </h2>
@ -134,7 +136,7 @@
<!-- Professional Info --> <!-- Professional Info -->
<div> <div>
<h2 <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 Professional Information
</h2> </h2>
@ -203,7 +205,7 @@
<!-- Community Connections --> <!-- Community Connections -->
<div> <div>
<h2 <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 Community Connections
</h2> </h2>
@ -219,7 +221,7 @@
<!-- Tags input --> <!-- Tags input -->
<div> <div>
<label <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 Skills & Topics
</label> </label>
@ -237,7 +239,7 @@
<span <span
v-for="(tag, index) in formData.offering.tags" v-for="(tag, index) in formData.offering.tags"
:key="tag" :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)" @click="removeOfferingTag(index)"
> >
{{ tag }} {{ tag }}
@ -251,7 +253,7 @@
<!-- Description textarea --> <!-- Description textarea -->
<div> <div>
<label <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 Details
</label> </label>
@ -281,7 +283,7 @@
<!-- Tags input --> <!-- Tags input -->
<div> <div>
<label <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 Skills & Topics
</label> </label>
@ -299,7 +301,7 @@
<span <span
v-for="(tag, index) in formData.lookingFor.tags" v-for="(tag, index) in formData.lookingFor.tags"
:key="tag" :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)" @click="removeLookingForTag(index)"
> >
{{ tag }} {{ tag }}
@ -313,7 +315,7 @@
<!-- Description textarea --> <!-- Description textarea -->
<div> <div>
<label <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 Details
</label> </label>
@ -338,7 +340,7 @@
<!-- Peer Support --> <!-- Peer Support -->
<div> <div>
<h2 <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 Peer Support
</h2> </h2>
@ -348,10 +350,14 @@
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
<USwitch v-model="formData.peerSupportEnabled" /> <USwitch v-model="formData.peerSupportEnabled" />
<div> <div>
<p class="font-medium text-ghost-200"> <p
class="font-medium text-gray-800 dark:text-ghost-200"
>
Offer Peer Support Offer Peer Support
</p> </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 Make yourself available to support other members
</p> </p>
</div> </div>
@ -385,7 +391,7 @@
topic, index topic, index
) in formData.peerSupportSkillTopics" ) in formData.peerSupportSkillTopics"
:key="topic" :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)" @click="removePeerSkillTopic(index)"
> >
{{ topic }} {{ topic }}
@ -454,7 +460,9 @@
class="w-full" class="w-full"
/> />
<template #hint> <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 {{ formData.peerSupportMessage?.length || 0 }}/200
characters characters
</span> </span>
@ -481,7 +489,7 @@
<!-- Directory Settings --> <!-- Directory Settings -->
<div> <div>
<h2 <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 Directory Visibility
</h2> </h2>
@ -489,10 +497,10 @@
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
<USwitch v-model="formData.showInDirectory" /> <USwitch v-model="formData.showInDirectory" />
<div> <div>
<p class="font-medium text-ghost-200"> <p class="font-medium text-gray-800 dark:text-ghost-200">
Show in Member Directory Show in Member Directory
</p> </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 Allow other members to discover and connect with you
through the directory through the directory
</p> </p>
@ -546,34 +554,42 @@
<!-- Current Membership --> <!-- Current Membership -->
<div> <div>
<h2 <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 Current Membership
</h2> </h2>
<div <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 class="flex items-start justify-between">
<div> <div>
<p class="text-sm text-ghost-400">Circle</p> <p class="text-sm text-gray-600 dark:text-ghost-400">
Circle
</p>
<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 }} {{ memberData.circle }}
</p> </p>
</div> </div>
<div> <div>
<p class="text-sm text-ghost-400">Contribution Level</p> <p class="text-sm text-gray-600 dark:text-ghost-400">
<p class="text-lg font-medium text-ghost-100"> Contribution Level
</p>
<p
class="text-lg font-medium text-gray-900 dark:text-ghost-100"
>
${{ contributionTierDetails?.amount }}/month ${{ contributionTierDetails?.amount }}/month
</p> </p>
</div> </div>
</div> </div>
<div v-if="memberData.subscriptionStartDate"> <div v-if="memberData.subscriptionStartDate">
<p class="text-sm text-ghost-400">Member Since</p> <p class="text-sm text-gray-600 dark:text-ghost-400">
<p class="text-ghost-100"> Member Since
</p>
<p class="text-gray-900 dark:text-ghost-100">
{{ formatDate(memberData.subscriptionStartDate) }} {{ formatDate(memberData.subscriptionStartDate) }}
</p> </p>
</div> </div>
@ -584,8 +600,10 @@
memberData.contributionTier !== '0' memberData.contributionTier !== '0'
" "
> >
<p class="text-sm text-ghost-400">Next Billing Date</p> <p class="text-sm text-gray-600 dark:text-ghost-400">
<p class="text-ghost-100"> Next Billing Date
</p>
<p class="text-gray-900 dark:text-ghost-100">
{{ formatDate(memberData.nextBillingDate) }} {{ formatDate(memberData.nextBillingDate) }}
</p> </p>
</div> </div>
@ -595,15 +613,15 @@
<!-- Change Contribution Level --> <!-- Change Contribution Level -->
<div> <div>
<h2 <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 Change Contribution Level
</h2> </h2>
<div <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. Choose a new contribution level that works for you.
Changes will take effect on your next billing cycle. Changes will take effect on your next billing cycle.
</p> </p>
@ -617,13 +635,15 @@
'w-full text-left p-4 rounded-lg border-2 transition-all', 'w-full text-left p-4 rounded-lg border-2 transition-all',
selectedContributionTier === tier.value selectedContributionTier === tier.value
? 'border-blue-400 bg-blue-500/20' ? '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" @click="selectedContributionTier = tier.value"
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="font-medium text-ghost-100"> <p
class="font-medium text-gray-900 dark:text-ghost-100"
>
{{ tier.label }} {{ tier.label }}
</p> </p>
</div> </div>
@ -669,20 +689,20 @@
<!-- Cancel Membership --> <!-- Cancel Membership -->
<div> <div>
<h2 <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 Cancel Membership
</h2> </h2>
<div <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 We're sorry to see you go. If you cancel, you'll lose
access to member benefits at the end of your current access to member benefits at the end of your current
billing period. billing period.
</p> </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. Need a break? Consider switching to the free tier instead.
</p> </p>
@ -1322,45 +1342,3 @@ useHead({
title: "Your Profile - Ghost Guild", title: "Your Profile - Ghost Guild",
}); });
</script> </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" size="medium"
/> />
<section class="py-12 px-4"> <section class="">
<UContainer class="px-4"> <UContainer class="px-4">
<!-- Search and Filters --> <!-- Search and Filters -->
<div class="mb-8 space-y-4"> <div class="mb-8 space-y-4">
@ -56,8 +56,8 @@
class="px-3 py-1 rounded-full text-sm transition-all border" class="px-3 py-1 rounded-full text-sm transition-all border"
:class=" :class="
selectedSkills.includes(skill) selectedSkills.includes(skill)
? 'bg-purple-500/20 text-purple-300 border-purple-500/50' ? 'bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 border-purple-300 dark:border-purple-500/50'
: 'bg-ghost-800/50 text-ghost-400 border-ghost-700 hover:border-ghost-600' : '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)" @click="toggleSkill(skill)"
> >
@ -94,8 +94,8 @@
class="px-3 py-1 rounded-full text-sm transition-all border" class="px-3 py-1 rounded-full text-sm transition-all border"
:class=" :class="
selectedTopics.includes(topic) selectedTopics.includes(topic)
? 'bg-purple-500/20 text-purple-300 border-purple-500/50' ? 'bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 border-purple-300 dark:border-purple-500/50'
: 'bg-ghost-800/50 text-ghost-400 border-ghost-700 hover:border-ghost-600' : '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)" @click="toggleTopic(topic)"
> >
@ -129,7 +129,7 @@
<span class="text-ghost-400">Active filters:</span> <span class="text-ghost-400">Active filters:</span>
<span <span
v-if="selectedCircle && selectedCircle !== 'all'" 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] }} {{ circleLabels[selectedCircle] }}
<button <button
@ -142,7 +142,7 @@
</span> </span>
<span <span
v-if="peerSupportFilter && peerSupportFilter !== 'all'" 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 Offering Peer Support
<button <button
@ -223,7 +223,7 @@
<div class="flex items-center gap-2 flex-wrap"> <div class="flex items-center gap-2 flex-wrap">
<span <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] }} {{ circleLabels[member.circle] }}
</span> </span>
@ -270,7 +270,7 @@
<span <span
v-for="topic in member.peerSupport.topics" v-for="topic in member.peerSupport.topics"
:key="topic" :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 }} {{ topic }}
</span> </span>
@ -301,7 +301,7 @@
<a <a
:href="`slack://user?team=T03A96LV4&id=${member.slackUserId}`" :href="`slack://user?team=T03A96LV4&id=${member.slackUserId}`"
@click.prevent="openSlackDM(member)" @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 Message {{ member.peerSupport.slackUsername }} on Slack
</a> </a>
@ -339,7 +339,7 @@
<span <span
v-for="tag in member.offering.tags" v-for="tag in member.offering.tags"
:key="tag" :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 }} {{ tag }}
</span> </span>
@ -367,7 +367,7 @@
<span <span
v-for="tag in member.lookingFor.tags" v-for="tag in member.lookingFor.tags"
:key="tag" :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 }} {{ tag }}
</span> </span>
@ -571,8 +571,14 @@ const openSlackDM = async (member) => {
window.open("https://gammaspace.slack.com", "_blank"); window.open("https://gammaspace.slack.com", "_blank");
}; };
// Load on mount // Load on mount and handle query params
onMounted(() => { onMounted(() => {
// Check for peerSupport query parameter
const route = useRoute();
if (route.query.peerSupport === "true") {
peerSupportFilter.value = "true";
}
loadMembers(); 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> <template>
<div> <div>
<!-- Hero Section --> <!-- Page Header -->
<div class="bg-gradient-to-br from-purple-600 via-blue-600 to-emerald-500 py-16"> <PageHeader
<UContainer> title="Event Series"
<div class="text-center"> subtitle="Discover our multi-event series designed to take you on a journey of learning and growth"
<h1 class="text-4xl md:text-6xl font-bold text-white mb-6"> theme="purple"
Event Series size="large"
</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>
<!-- Series Grid --> <!-- Series Grid -->
<div class="py-16 bg-gray-50"> <section class="py-20 bg-[--ui-bg]">
<UContainer> <UContainer>
<div v-if="pending" class="text-center py-12"> <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> <div
<p class="text-gray-600">Loading series...</p> 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>
<div v-else-if="filteredSeries.length > 0" class="space-y-8"> <div
<div v-else-if="filteredSeries.length > 0"
v-for="series in filteredSeries" class="max-w-4xl mx-auto space-y-6"
>
<div
v-for="series in filteredSeries"
:key="series.id" :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 --> <!-- Series Header -->
<div class="p-6 border-b border-gray-200"> <div class="p-6 border-b border-[--ui-border]">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4"> <div
class="flex flex-col md:flex-row md:items-start md:justify-between gap-4"
>
<div class="flex-1"> <div class="flex-1">
<div class="flex items-center gap-3 mb-2"> <div class="flex items-center gap-3 mb-3 flex-wrap">
<div :class="[ <span
'inline-flex items-center px-3 py-1 rounded-full text-sm font-medium', :class="[
getSeriesTypeBadgeClass(series.type) 'inline-flex items-center px-3 py-1 rounded text-sm font-medium',
]"> getSeriesTypeBadgeClass(series.type),
]"
>
{{ formatSeriesType(series.type) }} {{ formatSeriesType(series.type) }}
</div> </span>
<span :class="[ <span
'inline-flex items-center px-2 py-1 rounded-full text-xs font-medium', :class="[
series.status === 'active' ? 'bg-green-100 text-green-700' : 'inline-flex items-center px-2 py-1 rounded text-xs font-medium',
series.status === 'upcoming' ? 'bg-blue-100 text-blue-700' : series.status === 'active'
'bg-gray-100 text-gray-700' ? '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 }} {{ series.status }}
</span> </span>
</div> </div>
<h2 class="text-2xl font-bold text-gray-900 mb-2">{{ series.title }}</h2> <h2 class="text-2xl font-bold text-[--ui-text] mb-2">
<p class="text-gray-600 leading-relaxed">{{ series.description }}</p> {{ series.title }}
</h2>
<p class="text-[--ui-text-muted] leading-relaxed">
{{ series.description }}
</p>
</div> </div>
<div class="text-center md:text-right"> <div class="text-center md:text-right flex-shrink-0">
<div class="text-3xl font-bold text-purple-600 mb-1">{{ series.eventCount }}</div> <div class="text-3xl font-bold text-[--ui-text] mb-1">
<div class="text-sm text-gray-500">Events</div> {{ series.eventCount }}
<div v-if="series.totalEvents" class="text-xs text-gray-400 mt-1"> </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 of {{ series.totalEvents }} planned
</div> </div>
</div> </div>
@ -62,47 +78,61 @@
</div> </div>
<!-- Events List --> <!-- Events List -->
<div class="divide-y divide-gray-100"> <div class="divide-y divide-[--ui-border]">
<div <div
v-for="event in series.events" v-for="event in series.events"
:key="event.id" :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 justify-between gap-4">
<div class="flex items-center gap-4 flex-1"> <div class="flex items-center gap-4 flex-1 min-w-0">
<div class="w-10 h-10 bg-purple-100 text-purple-600 rounded-full flex items-center justify-center text-sm font-semibold"> <div
{{ event.series?.position || '?' }} 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>
<div class="flex-1"> <div class="flex-1 min-w-0">
<h3 class="font-medium text-gray-900 mb-1">{{ event.title }}</h3> <h3 class="font-medium text-[--ui-text] mb-1">
<div class="flex items-center gap-4 text-sm text-gray-500"> {{ 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"> <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) }} {{ formatEventDate(event.startDate) }}
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Icon name="heroicons:clock" class="w-4 h-4" /> <Icon name="heroicons:clock" class="w-4 h-4" />
{{ formatEventTime(event.startDate) }} {{ formatEventTime(event.startDate) }}
</div> </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" /> <Icon name="heroicons:users" class="w-4 h-4" />
{{ event.registrations.length }} registered {{ event.registrations.length }} registered
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3 flex-shrink-0">
<span :class="[ <span
'inline-flex items-center px-2 py-1 rounded-full text-xs font-medium', :class="[
getEventStatusClass(event) 'inline-flex items-center px-2 py-1 rounded text-xs font-medium',
]"> getEventStatusClass(event),
]"
>
{{ getEventStatus(event) }} {{ getEventStatus(event) }}
</span> </span>
<NuxtLink <NuxtLink
:to="`/events/${event.slug || event.id}`" :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> </NuxtLink>
</div> </div>
</div> </div>
@ -110,125 +140,167 @@
</div> </div>
<!-- Series Footer --> <!-- Series Footer -->
<div v-if="series.startDate && series.endDate" class="px-6 py-4 bg-gray-50 border-t border-gray-200"> <div
<div class="flex items-center justify-between text-sm text-gray-500"> class="px-6 py-4 bg-[--ui-bg-elevated] border-t border-[--ui-border]"
<div class="flex items-center gap-1"> >
<Icon name="heroicons:calendar-days" class="w-4 h-4" /> <div class="flex items-center justify-between gap-4">
Series runs from {{ formatDateRange(series.startDate, series.endDate) }} <div
</div> class="flex items-center gap-4 text-sm text-[--ui-text-muted] flex-wrap"
<div v-if="series.totalRegistrations" class="flex items-center gap-1"> >
<Icon name="heroicons:users" class="w-4 h-4" /> <div
{{ series.totalRegistrations }} total registrations 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> </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> </div>
</div> </div>
<div v-else class="text-center py-16"> <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" /> <Icon
<h3 class="text-xl font-semibold text-gray-900 mb-2">No Event Series Available</h3> name="heroicons:squares-2x2"
<p class="text-gray-600 max-w-md mx-auto"> class="w-16 h-16 text-[--ui-text-muted] mx-auto mb-4 opacity-50"
We're currently planning exciting event series. Check back soon for multi-event learning journeys! />
<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> </p>
</div> </div>
</UContainer> </UContainer>
</div> </section>
</div> </div>
</template> </template>
<script setup> <script setup>
// SEO // SEO
useHead({ useHead({
title: 'Event Series - Ghost Guild', title: "Event Series - Ghost Guild",
meta: [ 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 // Fetch series data
const { data: seriesData, pending } = await useFetch('/api/series', { const { data: seriesData, pending } = await useFetch("/api/series", {
query: { includeHidden: false } query: { includeHidden: false },
}) });
// Filter for active and upcoming series only // Filter for active and upcoming series only
const filteredSeries = computed(() => { const filteredSeries = computed(() => {
if (!seriesData.value) return [] if (!seriesData.value) return [];
return seriesData.value.filter(series => return seriesData.value.filter(
series.status === 'active' || series.status === 'upcoming' (series) => series.status === "active" || series.status === "upcoming",
) );
}) });
// Helper functions // Helper functions
const formatSeriesType = (type) => { const formatSeriesType = (type) => {
const types = { const types = {
'workshop_series': 'Workshop Series', workshop_series: "Workshop Series",
'recurring_meetup': 'Recurring Meetup', recurring_meetup: "Recurring Meetup",
'multi_day': 'Multi-Day Event', multi_day: "Multi-Day Event",
'course': 'Course', course: "Course",
'tournament': 'Tournament' tournament: "Tournament",
} };
return types[type] || type return types[type] || type;
} };
const getSeriesTypeBadgeClass = (type) => { const getSeriesTypeBadgeClass = (type) => {
const classes = { const classes = {
'workshop_series': 'bg-emerald-100 text-emerald-700', workshop_series:
'recurring_meetup': 'bg-blue-100 text-blue-700', "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30",
'multi_day': 'bg-purple-100 text-purple-700', recurring_meetup:
'course': 'bg-amber-100 text-amber-700', "bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/30",
'tournament': 'bg-red-100 text-red-700' multi_day:
} "bg-purple-500/10 text-purple-600 dark:text-purple-400 border border-purple-500/30",
return classes[type] || 'bg-gray-100 text-gray-700' 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) => { const formatEventDate = (date) => {
return new Date(date).toLocaleDateString('en-US', { return new Date(date).toLocaleDateString("en-US", {
weekday: 'long', month: "short",
month: 'long', day: "numeric",
day: 'numeric', year: "numeric",
year: 'numeric' });
}) };
}
const formatEventTime = (date) => { const formatEventTime = (date) => {
return new Date(date).toLocaleTimeString('en-US', { return new Date(date).toLocaleTimeString("en-US", {
hour: 'numeric', hour: "numeric",
minute: '2-digit', minute: "2-digit",
hour12: true hour12: true,
}) });
} };
const formatDateRange = (startDate, endDate) => { const formatDateRange = (startDate, endDate) => {
const start = new Date(startDate) const start = new Date(startDate);
const end = new Date(endDate) const end = new Date(endDate);
const formatter = new Intl.DateTimeFormat('en-US', { const formatter = new Intl.DateTimeFormat("en-US", {
month: 'long', month: "short",
day: 'numeric', day: "numeric",
year: 'numeric' year: "numeric",
}) });
return `${formatter.format(start)} to ${formatter.format(end)}` return `${formatter.format(start)} to ${formatter.format(end)}`;
} };
const getEventStatus = (event) => { const getEventStatus = (event) => {
const now = new Date() const now = new Date();
const startDate = new Date(event.startDate) const startDate = new Date(event.startDate);
const endDate = new Date(event.endDate) const endDate = new Date(event.endDate);
if (now < startDate) return 'Upcoming' if (now < startDate) return "Upcoming";
if (now >= startDate && now <= endDate) return 'Ongoing' if (now >= startDate && now <= endDate) return "Ongoing";
return 'Completed' return "Completed";
} };
const getEventStatusClass = (event) => { const getEventStatusClass = (event) => {
const status = getEventStatus(event) const status = getEventStatus(event);
const classes = { const classes = {
'Upcoming': 'bg-blue-100 text-blue-700', Upcoming:
'Ongoing': 'bg-green-100 text-green-700', "bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/30",
'Completed': 'bg-gray-100 text-gray-700' Ongoing:
} "bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/30",
return classes[status] || 'bg-gray-100 text-gray-700' Completed:
} "bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30",
</script> };
return (
classes[status] ||
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30"
);
};
</script>

14
assets/css/main.css Normal file
View file

@ -0,0 +1,14 @@
/* Global cursor pointer for all buttons and links */
button:not(:disabled):not([aria-disabled="true"]),
[role="button"]:not(:disabled):not([aria-disabled="true"]),
a[href] {
cursor: pointer !important;
}
/* Ensure disabled buttons show not-allowed cursor */
button:disabled,
button[aria-disabled="true"],
[role="button"]:disabled,
[role="button"][aria-disabled="true"] {
cursor: not-allowed !important;
}

View file

@ -3,13 +3,8 @@ export default defineNuxtConfig({
compatibilityDate: "2025-07-15", compatibilityDate: "2025-07-15",
devtools: { enabled: true }, devtools: { enabled: true },
modules: ["@nuxt/eslint", "@nuxt/ui", "@nuxtjs/plausible"], modules: ["@nuxt/eslint", "@nuxt/ui", "@nuxtjs/plausible"],
ui: {
theme: {
colors: ['primary', 'neutral', 'ghost', 'whisper', 'sparkle']
}
},
build: { build: {
transpile: ['vue-cal'] transpile: ["vue-cal"],
}, },
plausible: { plausible: {
domain: "ghostguild.org", domain: "ghostguild.org",
@ -17,20 +12,22 @@ export default defineNuxtConfig({
css: ["~/assets/css/main.css"], css: ["~/assets/css/main.css"],
runtimeConfig: { runtimeConfig: {
// Private keys (server-side only) // Private keys (server-side only)
mongodbUri: process.env.MONGODB_URI || 'mongodb://localhost:27017/ghostguild', mongodbUri:
jwtSecret: process.env.JWT_SECRET || 'dev-secret-change-in-production', process.env.MONGODB_URI || "mongodb://localhost:27017/ghostguild",
resendApiKey: process.env.RESEND_API_KEY || '', jwtSecret: process.env.JWT_SECRET || "dev-secret-change-in-production",
helcimApiToken: process.env.HELCIM_API_TOKEN || '', resendApiKey: process.env.RESEND_API_KEY || "",
slackBotToken: process.env.SLACK_BOT_TOKEN || '', helcimApiToken: process.env.HELCIM_API_TOKEN || "",
slackSigningSecret: process.env.SLACK_SIGNING_SECRET || '', slackBotToken: process.env.SLACK_BOT_TOKEN || "",
slackVettingChannelId: process.env.SLACK_VETTING_CHANNEL_ID || '', slackSigningSecret: process.env.SLACK_SIGNING_SECRET || "",
slackVettingChannelId: process.env.SLACK_VETTING_CHANNEL_ID || "",
// Public keys (available on client-side) // Public keys (available on client-side)
public: { public: {
helcimToken: process.env.NUXT_PUBLIC_HELCIM_TOKEN || '', helcimToken: process.env.NUXT_PUBLIC_HELCIM_TOKEN || "",
helcimAccountId: process.env.NUXT_PUBLIC_HELCIM_ACCOUNT_ID || '', helcimAccountId: process.env.NUXT_PUBLIC_HELCIM_ACCOUNT_ID || "",
cloudinaryCloudName: process.env.NUXT_PUBLIC_CLOUDINARY_CLOUD_NAME || 'divzuumlr', cloudinaryCloudName:
appUrl: process.env.NUXT_PUBLIC_APP_URL || 'http://localhost:3000' process.env.NUXT_PUBLIC_CLOUDINARY_CLOUD_NAME || "divzuumlr",
} appUrl: process.env.NUXT_PUBLIC_APP_URL || "http://localhost:3000",
} },
},
}); });