Redesign interface across member dashboard and events pages
The changes involve a comprehensive interface redesign across multiple pages, including: - Updated peer support badge with shield design - Switched privacy toggle to use USwitch component - Added light/dark mode support throughout - Enhanced layout and spacing in default template - Added series details page with timeline view - Improved event cards and status indicators - Refreshed member profile styles for better readability - Introduced global cursor styling for interactive elements
This commit is contained in:
parent
e8e3b84276
commit
896ad0336c
12 changed files with 915 additions and 360 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
446
app/pages/series/[id].vue
Normal 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>
|
||||
|
|
@ -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-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 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
|
||||
: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 start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
|
||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})
|
||||
const formatter = new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
return `${formatter.format(start)} to ${formatter.format(end)}`
|
||||
}
|
||||
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)
|
||||
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'
|
||||
}
|
||||
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'
|
||||
}
|
||||
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>
|
||||
14
assets/css/main.css
Normal file
14
assets/css/main.css
Normal 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;
|
||||
}
|
||||
|
|
@ -3,13 +3,8 @@ export default defineNuxtConfig({
|
|||
compatibilityDate: "2025-07-15",
|
||||
devtools: { enabled: true },
|
||||
modules: ["@nuxt/eslint", "@nuxt/ui", "@nuxtjs/plausible"],
|
||||
ui: {
|
||||
theme: {
|
||||
colors: ['primary', 'neutral', 'ghost', 'whisper', 'sparkle']
|
||||
}
|
||||
},
|
||||
build: {
|
||||
transpile: ['vue-cal']
|
||||
transpile: ["vue-cal"],
|
||||
},
|
||||
plausible: {
|
||||
domain: "ghostguild.org",
|
||||
|
|
@ -17,20 +12,22 @@ export default defineNuxtConfig({
|
|||
css: ["~/assets/css/main.css"],
|
||||
runtimeConfig: {
|
||||
// Private keys (server-side only)
|
||||
mongodbUri: process.env.MONGODB_URI || 'mongodb://localhost:27017/ghostguild',
|
||||
jwtSecret: process.env.JWT_SECRET || 'dev-secret-change-in-production',
|
||||
resendApiKey: process.env.RESEND_API_KEY || '',
|
||||
helcimApiToken: process.env.HELCIM_API_TOKEN || '',
|
||||
slackBotToken: process.env.SLACK_BOT_TOKEN || '',
|
||||
slackSigningSecret: process.env.SLACK_SIGNING_SECRET || '',
|
||||
slackVettingChannelId: process.env.SLACK_VETTING_CHANNEL_ID || '',
|
||||
mongodbUri:
|
||||
process.env.MONGODB_URI || "mongodb://localhost:27017/ghostguild",
|
||||
jwtSecret: process.env.JWT_SECRET || "dev-secret-change-in-production",
|
||||
resendApiKey: process.env.RESEND_API_KEY || "",
|
||||
helcimApiToken: process.env.HELCIM_API_TOKEN || "",
|
||||
slackBotToken: process.env.SLACK_BOT_TOKEN || "",
|
||||
slackSigningSecret: process.env.SLACK_SIGNING_SECRET || "",
|
||||
slackVettingChannelId: process.env.SLACK_VETTING_CHANNEL_ID || "",
|
||||
|
||||
// Public keys (available on client-side)
|
||||
public: {
|
||||
helcimToken: process.env.NUXT_PUBLIC_HELCIM_TOKEN || '',
|
||||
helcimAccountId: process.env.NUXT_PUBLIC_HELCIM_ACCOUNT_ID || '',
|
||||
cloudinaryCloudName: process.env.NUXT_PUBLIC_CLOUDINARY_CLOUD_NAME || 'divzuumlr',
|
||||
appUrl: process.env.NUXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
||||
}
|
||||
}
|
||||
helcimToken: process.env.NUXT_PUBLIC_HELCIM_TOKEN || "",
|
||||
helcimAccountId: process.env.NUXT_PUBLIC_HELCIM_ACCOUNT_ID || "",
|
||||
cloudinaryCloudName:
|
||||
process.env.NUXT_PUBLIC_CLOUDINARY_CLOUD_NAME || "divzuumlr",
|
||||
appUrl: process.env.NUXT_PUBLIC_APP_URL || "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue