Adding features
This commit is contained in:
parent
600fef2b7c
commit
2b55ca4104
75 changed files with 9796 additions and 2759 deletions
|
|
@ -1,83 +1,105 @@
|
|||
<template>
|
||||
<div v-if="pending" class="min-h-screen flex items-center justify-center">
|
||||
<div
|
||||
v-if="pending"
|
||||
class="min-h-screen bg-stone-900 flex items-center justify-center"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
||||
<p class="text-gray-600 dark:text-gray-400">Loading event details...</p>
|
||||
<div
|
||||
class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"
|
||||
></div>
|
||||
<p class="text-stone-200">Loading event details...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="min-h-screen flex items-center justify-center">
|
||||
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="min-h-screen bg-stone-900 flex items-center justify-center"
|
||||
>
|
||||
<div class="text-center">
|
||||
<Icon name="heroicons:exclamation-triangle" class="w-16 h-16 text-red-500 mx-auto mb-4" />
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Event Not Found</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-6">The event you're looking for doesn't exist.</p>
|
||||
<NuxtLink to="/events" class="text-blue-600 dark:text-blue-400 hover:underline">
|
||||
<Icon
|
||||
name="heroicons:exclamation-triangle"
|
||||
class="w-16 h-16 text-red-500 mx-auto mb-4"
|
||||
/>
|
||||
<h2 class="text-2xl font-bold text-stone-100 mb-2">Event Not Found</h2>
|
||||
<p class="text-stone-300 mb-6">
|
||||
The event you're looking for doesn't exist.
|
||||
</p>
|
||||
<NuxtLink to="/events" class="text-blue-400 hover:underline">
|
||||
← Back to Events
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-else>
|
||||
<!-- Feature Image Header -->
|
||||
<div v-if="event.featureImage && (event.featureImage.publicId || event.featureImage.url)" class="relative h-96 overflow-hidden">
|
||||
<img
|
||||
<div
|
||||
v-if="
|
||||
event.featureImage &&
|
||||
(event.featureImage.publicId || event.featureImage.url)
|
||||
"
|
||||
class="relative h-96 overflow-hidden"
|
||||
>
|
||||
<img
|
||||
:src="getImageUrl(event.featureImage)"
|
||||
:alt="event.featureImage.alt || event.title"
|
||||
class="w-full h-full object-cover"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div class="absolute inset-0" style="background-color: rgba(0, 0, 0, 0.4);"></div>
|
||||
<div
|
||||
class="absolute inset-0"
|
||||
style="background-color: rgba(0, 0, 0, 0.4)"
|
||||
></div>
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<UContainer>
|
||||
<div class="max-w-4xl">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||
{{ event.title }}
|
||||
</h1>
|
||||
<p v-if="event.tagline" class="text-xl text-gray-200">
|
||||
{{ event.tagline }}
|
||||
</p>
|
||||
</div>
|
||||
</UContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page Header (fallback when no image) -->
|
||||
<PageHeader
|
||||
v-else
|
||||
:title="event.title"
|
||||
:subtitle="event.tagline"
|
||||
theme="blue"
|
||||
size="medium"
|
||||
/>
|
||||
<PageHeader v-else :title="event.title" theme="blue" size="medium" />
|
||||
|
||||
<!-- Event Details Section -->
|
||||
<section class="py-16 bg-white dark:bg-gray-900">
|
||||
<section class="py-16 bg-stone-900">
|
||||
<UContainer>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- Event Meta Info -->
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-xl p-6 mb-8 border border-blue-200 dark:border-blue-800">
|
||||
<div class="bg-stone-800 rounded-xl p-6 mb-8 border border-stone-700">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="flex items-center space-x-3">
|
||||
<Icon name="heroicons:calendar-days" class="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||
<Icon
|
||||
name="heroicons:calendar-days"
|
||||
class="w-6 h-6 text-blue-400"
|
||||
/>
|
||||
<div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Date</p>
|
||||
<p class="font-semibold text-gray-900 dark:text-white">{{ formatDate(event.startDate) }}</p>
|
||||
<p class="text-sm text-stone-400">Date</p>
|
||||
<p class="font-semibold text-stone-100">
|
||||
{{ formatDate(event.startDate) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<Icon name="heroicons:clock" class="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||
<Icon name="heroicons:clock" class="w-6 h-6 text-blue-400" />
|
||||
<div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Time</p>
|
||||
<p class="font-semibold text-gray-900 dark:text-white">{{ formatTime(event.startDate, event.endDate) }}</p>
|
||||
<p class="text-sm text-stone-400">Time</p>
|
||||
<p class="font-semibold text-stone-100">
|
||||
{{ formatTime(event.startDate, event.endDate) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<Icon name="heroicons:map-pin" class="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||
<Icon name="heroicons:map-pin" class="w-6 h-6 text-blue-400" />
|
||||
<div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Location</p>
|
||||
<p class="font-semibold text-gray-900 dark:text-white">{{ event.location }}</p>
|
||||
<p class="text-sm text-stone-400">Location</p>
|
||||
<p class="font-semibold text-stone-100">
|
||||
{{ event.location }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -85,16 +107,22 @@
|
|||
|
||||
<!-- Event Cancelled Notice -->
|
||||
<div v-if="event.isCancelled" class="mb-8">
|
||||
<div class="p-6 bg-red-50 dark:bg-red-900/20 rounded-xl border border-red-200 dark:border-red-800">
|
||||
<div class="p-6 bg-red-900/20 rounded-xl border border-red-800">
|
||||
<div class="flex items-start">
|
||||
<Icon name="heroicons:exclamation-triangle" class="w-6 h-6 text-red-600 dark:text-red-400 mr-3 mt-0.5" />
|
||||
<Icon
|
||||
name="heroicons:exclamation-triangle"
|
||||
class="w-6 h-6 text-red-400 mr-3 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-red-700 dark:text-red-300 mb-2">Event Cancelled</h3>
|
||||
<p class="text-red-600 dark:text-red-400" v-if="event.cancellationMessage">
|
||||
<h3 class="text-lg font-semibold text-red-300 mb-2">
|
||||
Event Cancelled
|
||||
</h3>
|
||||
<p class="text-red-400" v-if="event.cancellationMessage">
|
||||
{{ event.cancellationMessage }}
|
||||
</p>
|
||||
<p class="text-red-600 dark:text-red-400" v-else>
|
||||
This event has been cancelled. We apologize for any inconvenience.
|
||||
<p class="text-red-400" v-else>
|
||||
This event has been cancelled. We apologize for any
|
||||
inconvenience.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -103,24 +131,36 @@
|
|||
|
||||
<!-- Member-Only Badge -->
|
||||
<div v-if="event.membersOnly" class="mb-8">
|
||||
<div class="inline-flex items-center px-4 py-2 bg-purple-100 dark:bg-purple-900/30 rounded-full">
|
||||
<Icon name="heroicons:lock-closed" class="w-5 h-5 text-purple-600 dark:text-purple-400 mr-2" />
|
||||
<span class="text-sm font-medium text-purple-700 dark:text-purple-300">
|
||||
<div
|
||||
class="inline-flex items-center px-4 py-2 bg-purple-100 dark:bg-purple-900/30 rounded-full"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:lock-closed"
|
||||
class="w-5 h-5 text-purple-600 dark:text-purple-400 mr-2"
|
||||
/>
|
||||
<span
|
||||
class="text-sm font-medium text-purple-700 dark:text-purple-300"
|
||||
>
|
||||
Members Only Event - Open to all circles and contribution levels
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Target Circles -->
|
||||
<div v-if="event.targetCircles && event.targetCircles.length > 0" class="mb-8">
|
||||
<div
|
||||
v-if="event.targetCircles && event.targetCircles.length > 0"
|
||||
class="mb-8"
|
||||
>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Icon name="heroicons:user-group" class="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Recommended for:</span>
|
||||
<Icon name="heroicons:user-group" class="w-5 h-5 text-blue-400" />
|
||||
<span class="text-sm font-medium text-stone-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-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
|
||||
<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"
|
||||
>
|
||||
{{ formatCircleName(circle) }}
|
||||
</span>
|
||||
|
|
@ -130,32 +170,64 @@
|
|||
|
||||
<!-- Event Description -->
|
||||
<div class="prose prose-lg dark:prose-invert max-w-none mb-12">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">About This Event</h2>
|
||||
<p class="text-gray-700 dark:text-gray-300">{{ event.description }}</p>
|
||||
|
||||
<h2 class="text-2xl font-bold text-stone-100 mb-4">
|
||||
About This Event
|
||||
</h2>
|
||||
<p class="text-stone-200">
|
||||
{{ event.description }}
|
||||
</p>
|
||||
|
||||
<div v-if="event.agenda && event.agenda.length > 0" class="mt-8">
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Event Agenda</h3>
|
||||
<h3 class="text-xl font-semibold text-stone-100 mb-4">
|
||||
Event Agenda
|
||||
</h3>
|
||||
<ul class="space-y-3">
|
||||
<li v-for="(item, index) in event.agenda" :key="index" class="flex items-start">
|
||||
<span class="inline-block w-6 h-6 bg-blue-500 text-white text-sm rounded-full flex items-center justify-center mr-3 mt-0.5">
|
||||
<li
|
||||
v-for="(item, index) in event.agenda"
|
||||
:key="index"
|
||||
class="flex items-start"
|
||||
>
|
||||
<span
|
||||
class="inline-block w-6 h-6 bg-blue-500 text-white text-sm rounded-full flex items-center justify-center mr-3 mt-0.5"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ item }}</span>
|
||||
<span class="text-stone-200">{{ item }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="event.speakers && event.speakers.length > 0" class="mt-8">
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Speakers</h3>
|
||||
<div
|
||||
v-if="event.speakers && event.speakers.length > 0"
|
||||
class="mt-8"
|
||||
>
|
||||
<h3 class="text-xl font-semibold text-stone-100 mb-4">
|
||||
Speakers
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div v-for="speaker in event.speakers" :key="speaker.name" class="flex items-start space-x-4">
|
||||
<div class="w-16 h-16 bg-gray-200 dark:bg-gray-700 rounded-full flex items-center justify-center">
|
||||
<Icon name="heroicons:user" class="w-8 h-8 text-gray-400 dark:text-gray-500" />
|
||||
<div
|
||||
v-for="speaker in event.speakers"
|
||||
:key="speaker.name"
|
||||
class="flex items-start space-x-4"
|
||||
>
|
||||
<div
|
||||
class="w-16 h-16 bg-stone-700 rounded-full flex items-center justify-center"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:user"
|
||||
class="w-8 h-8 text-stone-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-gray-900 dark:text-white">{{ speaker.name }}</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{{ speaker.role }}</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-500 mt-1">{{ speaker.bio }}</p>
|
||||
<p class="font-semibold text-stone-100">
|
||||
{{ speaker.name }}
|
||||
</p>
|
||||
<p class="text-sm text-stone-300">
|
||||
{{ speaker.role }}
|
||||
</p>
|
||||
<p class="text-sm text-stone-400 mt-1">
|
||||
{{ speaker.bio }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -163,30 +235,75 @@
|
|||
</div>
|
||||
|
||||
<!-- Registration Section -->
|
||||
<div v-if="!event.isCancelled" class="bg-gray-50 dark:bg-gray-800 rounded-xl p-8 border border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-6">Register for This Event</h3>
|
||||
|
||||
<div
|
||||
v-if="!event.isCancelled"
|
||||
class="bg-stone-800 rounded-xl p-8 border border-stone-700"
|
||||
>
|
||||
<h3 class="text-xl font-bold text-stone-100 mb-6">
|
||||
Register for This Event
|
||||
</h3>
|
||||
|
||||
<!-- Registration Status -->
|
||||
<div v-if="registrationStatus === 'registered'" class="mb-6">
|
||||
<div class="flex items-center p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<Icon name="heroicons:check-circle" class="w-6 h-6 text-green-600 dark:text-green-400 mr-3" />
|
||||
<div>
|
||||
<p class="font-semibold text-green-700 dark:text-green-300">You're registered!</p>
|
||||
<p class="text-sm text-green-600 dark:text-green-400">We've sent a confirmation to your email</p>
|
||||
<div
|
||||
class="p-4 bg-green-900/20 rounded-lg border border-green-800"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-center">
|
||||
<Icon
|
||||
name="heroicons:check-circle"
|
||||
class="w-6 h-6 text-green-400 mr-3"
|
||||
/>
|
||||
<div>
|
||||
<p class="font-semibold text-green-300">
|
||||
You're registered!
|
||||
</p>
|
||||
<p class="text-sm text-green-400">
|
||||
We've sent a confirmation to your email
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<UButton
|
||||
color="red"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="handleCancelRegistration"
|
||||
:loading="isCancelling"
|
||||
>
|
||||
Cancel Registration
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Member Gate Warning -->
|
||||
<div v-if="event.membersOnly && !isMember && registrationStatus !== 'registered'" class="mb-6">
|
||||
<div class="flex items-start p-4 bg-amber-50 dark:bg-amber-900/20 rounded-lg border border-amber-200 dark:border-amber-800">
|
||||
<Icon name="heroicons:exclamation-triangle" class="w-6 h-6 text-amber-600 dark:text-amber-400 mr-3 mt-0.5" />
|
||||
<div
|
||||
v-if="
|
||||
event.membersOnly &&
|
||||
!isMember &&
|
||||
registrationStatus !== 'registered'
|
||||
"
|
||||
class="mb-6"
|
||||
>
|
||||
<div
|
||||
class="flex items-start p-4 bg-amber-900/20 rounded-lg border border-amber-800"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:exclamation-triangle"
|
||||
class="w-6 h-6 text-amber-400 mr-3 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<p class="font-semibold text-amber-700 dark:text-amber-300">Membership Required</p>
|
||||
<p class="text-sm text-amber-600 dark:text-amber-400 mt-1">
|
||||
This event is exclusive to Ghost Guild members. Join any circle to gain access.
|
||||
<p class="font-semibold text-amber-300">
|
||||
Membership Required
|
||||
</p>
|
||||
<NuxtLink to="/join" class="inline-flex items-center text-sm font-medium text-amber-700 dark:text-amber-300 hover:underline mt-2">
|
||||
<p class="text-sm text-amber-400 mt-1">
|
||||
This event is exclusive to Ghost Guild members. Join any
|
||||
circle to gain access.
|
||||
</p>
|
||||
<NuxtLink
|
||||
to="/join"
|
||||
class="inline-flex items-center text-sm font-medium text-amber-300 hover:underline mt-2"
|
||||
>
|
||||
Become a member
|
||||
<Icon name="heroicons:arrow-right" class="w-4 h-4 ml-1" />
|
||||
</NuxtLink>
|
||||
|
|
@ -195,40 +312,53 @@
|
|||
</div>
|
||||
|
||||
<!-- Registration Form -->
|
||||
<form v-if="registrationStatus !== 'registered'" @submit.prevent="handleRegistration" class="space-y-4">
|
||||
<form
|
||||
v-if="registrationStatus !== 'registered'"
|
||||
@submit.prevent="handleRegistration"
|
||||
class="space-y-4"
|
||||
>
|
||||
<!-- Show form fields only for public events OR for logged-in members -->
|
||||
<template v-if="!event.membersOnly || isMember">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<label
|
||||
for="name"
|
||||
class="block text-sm font-medium text-stone-200 mb-2"
|
||||
>
|
||||
Full Name
|
||||
</label>
|
||||
<UInput
|
||||
id="name"
|
||||
v-model="registrationForm.name"
|
||||
type="text"
|
||||
required
|
||||
<UInput
|
||||
id="name"
|
||||
v-model="registrationForm.name"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Enter your full name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<label
|
||||
for="email"
|
||||
class="block text-sm font-medium text-stone-200 mb-2"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
<UInput
|
||||
id="email"
|
||||
v-model="registrationForm.email"
|
||||
type="email"
|
||||
required
|
||||
<UInput
|
||||
id="email"
|
||||
v-model="registrationForm.email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="Enter your email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="membershipLevel" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<label
|
||||
for="membershipLevel"
|
||||
class="block text-sm font-medium text-stone-200 mb-2"
|
||||
>
|
||||
Membership Status
|
||||
</label>
|
||||
<USelect
|
||||
<USelect
|
||||
id="membershipLevel"
|
||||
v-model="registrationForm.membershipLevel"
|
||||
:options="membershipOptions"
|
||||
|
|
@ -236,28 +366,19 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<div class="pt-4">
|
||||
<UButton
|
||||
<UButton
|
||||
v-if="!event.membersOnly || isMember"
|
||||
type="submit"
|
||||
color="primary"
|
||||
size="lg"
|
||||
type="submit"
|
||||
color="primary"
|
||||
size="lg"
|
||||
block
|
||||
:loading="isRegistering"
|
||||
>
|
||||
{{ isRegistering ? 'Registering...' : 'Register for Event' }}
|
||||
{{ isRegistering ? "Registering..." : "Register for Event" }}
|
||||
</UButton>
|
||||
<NuxtLink
|
||||
v-else
|
||||
to="/join"
|
||||
class="block"
|
||||
>
|
||||
<UButton
|
||||
color="primary"
|
||||
size="lg"
|
||||
block
|
||||
>
|
||||
<NuxtLink v-else to="/join" class="block">
|
||||
<UButton color="primary" size="lg" block>
|
||||
Become a Member to Register
|
||||
</UButton>
|
||||
</NuxtLink>
|
||||
|
|
@ -265,15 +386,20 @@
|
|||
</form>
|
||||
|
||||
<!-- Event Capacity -->
|
||||
<div v-if="event.maxAttendees" class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<div
|
||||
v-if="event.maxAttendees"
|
||||
class="mt-6 pt-6 border-t border-stone-700"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">Event Capacity</span>
|
||||
<span class="text-sm text-stone-300">Event Capacity</span>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<span class="text-sm font-semibold text-stone-100">
|
||||
{{ event.registeredCount || 0 }} / {{ event.maxAttendees }}
|
||||
</span>
|
||||
<div class="w-24 h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
<div
|
||||
class="w-24 h-2 bg-stone-700 rounded-full overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="h-full bg-blue-500 rounded-full"
|
||||
:style="`width: ${((event.registeredCount || 0) / event.maxAttendees) * 100}%`"
|
||||
/>
|
||||
|
|
@ -284,12 +410,15 @@
|
|||
</div>
|
||||
|
||||
<!-- Additional Information -->
|
||||
<div class="mt-8 p-6 bg-blue-50 dark:bg-blue-900/20 rounded-xl border border-blue-200 dark:border-blue-800">
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-3">Questions?</h4>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300 mb-3">
|
||||
If you have any questions about this event, please reach out to our events team.
|
||||
<div class="mt-8 p-6 bg-stone-800 rounded-xl border border-stone-700">
|
||||
<h4 class="font-semibold text-stone-100 mb-3">Questions?</h4>
|
||||
<p class="text-sm text-stone-200 mb-3">
|
||||
If you have any questions about this event please drop us a line.
|
||||
</p>
|
||||
<a href="mailto:events@ghostguild.org" class="inline-flex items-center text-blue-600 dark:text-blue-400 hover:underline">
|
||||
<a
|
||||
href="mailto:events@ghostguild.org"
|
||||
class="inline-flex items-center text-blue-400 hover:underline"
|
||||
>
|
||||
<Icon name="heroicons:envelope" class="w-4 h-4 mr-2" />
|
||||
events@ghostguild.org
|
||||
</a>
|
||||
|
|
@ -301,164 +430,254 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
const route = useRoute();
|
||||
const toast = useToast();
|
||||
|
||||
// Fetch event data from API
|
||||
const { data: event, pending, error } = await useFetch(`/api/events/${route.params.id}`)
|
||||
const {
|
||||
data: event,
|
||||
pending,
|
||||
error,
|
||||
} = await useFetch(`/api/events/${route.params.id}`);
|
||||
|
||||
// Handle event not found
|
||||
if (error.value?.statusCode === 404) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Event not found'
|
||||
})
|
||||
statusMessage: "Event not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Authentication
|
||||
const { isMember, memberData, checkMemberStatus } = useAuth()
|
||||
const { isMember, memberData, checkMemberStatus } = useAuth();
|
||||
|
||||
// Check member status on mount
|
||||
onMounted(async () => {
|
||||
await checkMemberStatus()
|
||||
|
||||
await checkMemberStatus();
|
||||
|
||||
// Pre-fill form if member is logged in
|
||||
if (memberData.value) {
|
||||
registrationForm.value.name = memberData.value.name
|
||||
registrationForm.value.email = memberData.value.email
|
||||
registrationForm.value.membershipLevel = memberData.value.membershipLevel || 'non-member'
|
||||
registrationForm.value.name = memberData.value.name;
|
||||
registrationForm.value.email = memberData.value.email;
|
||||
registrationForm.value.membershipLevel =
|
||||
memberData.value.membershipLevel || "non-member";
|
||||
|
||||
// Check if user is already registered
|
||||
await checkRegistrationStatus();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Check if user is already registered for this event
|
||||
const checkRegistrationStatus = async () => {
|
||||
if (!memberData.value?.email) return;
|
||||
|
||||
try {
|
||||
const response = await $fetch(
|
||||
`/api/events/${route.params.id}/check-registration`,
|
||||
{
|
||||
method: "POST",
|
||||
body: { email: memberData.value.email },
|
||||
},
|
||||
);
|
||||
|
||||
if (response.isRegistered) {
|
||||
registrationStatus.value = "registered";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to check registration status:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Registration form state
|
||||
const registrationForm = ref({
|
||||
name: '',
|
||||
email: '',
|
||||
membershipLevel: 'non-member'
|
||||
})
|
||||
name: "",
|
||||
email: "",
|
||||
membershipLevel: "non-member",
|
||||
});
|
||||
|
||||
const membershipOptions = [
|
||||
{ label: 'Non-member', value: 'non-member' },
|
||||
{ label: 'Circle of Community', value: 'community' },
|
||||
{ label: 'Circle of Founders', value: 'founder' },
|
||||
{ label: 'Circle of Practitioners', value: 'practitioner' }
|
||||
]
|
||||
{ label: "Non-member", value: "non-member" },
|
||||
{ label: "Circle of Community", value: "community" },
|
||||
{ label: "Circle of Founders", value: "founder" },
|
||||
{ label: "Circle of Practitioners", value: "practitioner" },
|
||||
];
|
||||
|
||||
const isRegistering = ref(false)
|
||||
const registrationStatus = ref('not-registered') // 'not-registered', 'registered'
|
||||
const isRegistering = ref(false);
|
||||
const isCancelling = ref(false);
|
||||
const registrationStatus = ref("not-registered"); // 'not-registered', 'registered'
|
||||
|
||||
// Format date for display
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString)
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
}).format(date)
|
||||
}
|
||||
const date = new Date(dateString);
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
// Format time range for display
|
||||
const formatTime = (startDate, endDate) => {
|
||||
const start = new Date(startDate)
|
||||
const end = new Date(endDate)
|
||||
|
||||
const timeFormat = new Intl.DateTimeFormat('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
timeZoneName: 'short'
|
||||
})
|
||||
|
||||
return `${timeFormat.format(start)} - ${timeFormat.format(end)}`
|
||||
}
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
|
||||
const timeFormat = new Intl.DateTimeFormat("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
timeZoneName: "short",
|
||||
});
|
||||
|
||||
return `${timeFormat.format(start)} - ${timeFormat.format(end)}`;
|
||||
};
|
||||
|
||||
// Format circle name for display
|
||||
const formatCircleName = (circleValue) => {
|
||||
const circleNames = {
|
||||
'community': 'Community Circle',
|
||||
'founder': 'Founder Circle',
|
||||
'practitioner': 'Practitioner Circle'
|
||||
}
|
||||
return circleNames[circleValue] || circleValue
|
||||
}
|
||||
community: "Community Circle",
|
||||
founder: "Founder Circle",
|
||||
practitioner: "Practitioner Circle",
|
||||
};
|
||||
return circleNames[circleValue] || circleValue;
|
||||
};
|
||||
|
||||
// Get optimized Cloudinary image URL
|
||||
const getOptimizedImageUrl = (publicId, transformations) => {
|
||||
if (!publicId) return ''
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
return `https://res.cloudinary.com/${config.public.cloudinaryCloudName}/image/upload/${transformations}/f_auto,q_auto/${publicId}`
|
||||
}
|
||||
if (!publicId) return "";
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
return `https://res.cloudinary.com/${config.public.cloudinaryCloudName}/image/upload/${transformations}/f_auto,q_auto/${publicId}`;
|
||||
};
|
||||
|
||||
// Get image URL with fallback logic
|
||||
const getImageUrl = (featureImage) => {
|
||||
if (!featureImage) return ''
|
||||
|
||||
if (!featureImage) return "";
|
||||
|
||||
// If we have a direct URL, use it as primary (since seed data uses external URLs)
|
||||
if (featureImage.url) {
|
||||
return featureImage.url
|
||||
return featureImage.url;
|
||||
}
|
||||
|
||||
|
||||
// Fallback to Cloudinary if we have a publicId
|
||||
if (featureImage.publicId) {
|
||||
return getOptimizedImageUrl(featureImage.publicId, 'w_1200,h_400,c_fill')
|
||||
return getOptimizedImageUrl(featureImage.publicId, "w_1200,h_400,c_fill");
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
// Handle image loading errors
|
||||
const handleImageError = (event) => {
|
||||
console.warn('Image failed to load:', event.target.src)
|
||||
console.warn("Image failed to load:", event.target.src);
|
||||
// Optionally hide the image container or show a placeholder
|
||||
}
|
||||
};
|
||||
|
||||
// Handle registration submission
|
||||
const handleRegistration = async () => {
|
||||
isRegistering.value = true
|
||||
|
||||
isRegistering.value = true;
|
||||
|
||||
try {
|
||||
// Submit registration to API using slug or ID
|
||||
const response = await $fetch(`/api/events/${route.params.id}/register`, {
|
||||
method: 'POST',
|
||||
body: registrationForm.value
|
||||
})
|
||||
|
||||
method: "POST",
|
||||
body: registrationForm.value,
|
||||
});
|
||||
|
||||
// Update registration status
|
||||
registrationStatus.value = 'registered'
|
||||
|
||||
registrationStatus.value = "registered";
|
||||
|
||||
// Show success toast
|
||||
toast.add({
|
||||
title: 'Registration Successful!',
|
||||
title: "Registration Successful!",
|
||||
description: `You're registered for ${event.value.title}. Check your email for confirmation.`,
|
||||
color: 'green'
|
||||
})
|
||||
|
||||
color: "green",
|
||||
});
|
||||
|
||||
// Update registered count
|
||||
if (event.value.registeredCount !== undefined) {
|
||||
event.value.registeredCount++
|
||||
event.value.registeredCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Registration failed:', error)
|
||||
|
||||
console.error("Registration failed:", error);
|
||||
|
||||
// Handle specific error messages
|
||||
const errorMessage = error.data?.statusMessage || 'Something went wrong. Please try again.'
|
||||
|
||||
const errorMessage =
|
||||
error.data?.statusMessage || "Something went wrong. Please try again.";
|
||||
|
||||
toast.add({
|
||||
title: 'Registration Failed',
|
||||
title: "Registration Failed",
|
||||
description: errorMessage,
|
||||
color: 'red'
|
||||
})
|
||||
color: "red",
|
||||
});
|
||||
} finally {
|
||||
isRegistering.value = false
|
||||
isRegistering.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle registration cancellation
|
||||
const handleCancelRegistration = async () => {
|
||||
if (
|
||||
!confirm(
|
||||
"Are you sure you want to cancel your registration for this event?",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
isCancelling.value = true;
|
||||
|
||||
try {
|
||||
const response = await $fetch(
|
||||
`/api/events/${route.params.id}/cancel-registration`,
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
email: registrationForm.value.email || memberData.value?.email,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Update registration status
|
||||
registrationStatus.value = "not-registered";
|
||||
|
||||
// Show success toast
|
||||
toast.add({
|
||||
title: "Registration Cancelled",
|
||||
description: "Your registration has been cancelled successfully.",
|
||||
color: "blue",
|
||||
});
|
||||
|
||||
// Update registered count
|
||||
if (event.value.registeredCount !== undefined) {
|
||||
event.value.registeredCount--;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Cancel registration failed:", error);
|
||||
|
||||
const errorMessage =
|
||||
error.data?.statusMessage ||
|
||||
"Failed to cancel registration. Please try again.";
|
||||
|
||||
toast.add({
|
||||
title: "Cancellation Failed",
|
||||
description: errorMessage,
|
||||
color: "red",
|
||||
});
|
||||
} finally {
|
||||
isCancelling.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// SEO Meta
|
||||
useHead(() => ({
|
||||
title: event.value ? `${event.value.title} - Ghost Guild Events` : 'Event - Ghost Guild',
|
||||
title: event.value
|
||||
? `${event.value.title} - Ghost Guild Events`
|
||||
: "Event - Ghost Guild",
|
||||
meta: [
|
||||
{ name: 'description', content: event.value?.description || 'View event details and register' }
|
||||
]
|
||||
}))
|
||||
</script>
|
||||
{
|
||||
name: "description",
|
||||
content: event.value?.description || "View event details and register",
|
||||
},
|
||||
],
|
||||
}));
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,141 +1,219 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- Page Header -->
|
||||
<PageHeader
|
||||
<PageHeader
|
||||
title="Events"
|
||||
subtitle="Join our community events, workshops, and gatherings designed to connect developers and share knowledge about cooperative game development."
|
||||
theme="blue"
|
||||
subtitle="Join our community events, workshops, and gatherings"
|
||||
size="large"
|
||||
/>
|
||||
|
||||
<!-- Event Calendar -->
|
||||
<section class="py-20 bg-white dark:bg-gray-900">
|
||||
<!-- Events Section with Tabs -->
|
||||
<section class="py-20 bg-stone-900 dark:bg-stone-950">
|
||||
<UContainer>
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl font-bold text-blue-600 dark:text-blue-400 mb-8">
|
||||
Event Calendar
|
||||
</h2>
|
||||
<div class="flex items-center justify-center gap-2 mb-8">
|
||||
<div class="w-6 h-6 bg-blue-500 rounded-full" />
|
||||
<div class="w-6 h-6 bg-blue-400 rounded-full" />
|
||||
<div class="w-8 h-1 bg-blue-300 rounded-full" />
|
||||
<div class="w-8 h-1 bg-blue-200 rounded-full" />
|
||||
<div class="w-8 h-1 bg-blue-100 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<div class="bg-gray-50 dark:bg-gray-800 rounded-2xl p-6 border border-gray-200 dark:border-gray-700">
|
||||
<ClientOnly>
|
||||
<div v-if="pending" class="min-h-[400px] bg-gray-100 dark:bg-gray-700 rounded-xl flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
||||
<p class="text-gray-600 dark:text-gray-400">Loading events...</p>
|
||||
</div>
|
||||
</div>
|
||||
<VueCal
|
||||
v-else
|
||||
:events="events"
|
||||
:time="false"
|
||||
active-view="month"
|
||||
class="custom-calendar"
|
||||
:disable-views="['years', 'year']"
|
||||
:hide-weekends="false"
|
||||
today-button
|
||||
events-on-month-view="short"
|
||||
:editable-events="{
|
||||
title: false,
|
||||
drag: false,
|
||||
resize: false,
|
||||
delete: false,
|
||||
create: false
|
||||
}"
|
||||
@event-click="onEventClick"
|
||||
/>
|
||||
<template #fallback>
|
||||
<div class="min-h-[400px] bg-gray-100 dark:bg-gray-700 rounded-xl flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
||||
<p class="text-gray-600 dark:text-gray-400">Loading calendar...</p>
|
||||
<UTabs
|
||||
v-model="activeTab"
|
||||
:items="[
|
||||
{ label: 'Upcoming Events', value: 'upcoming', slot: 'upcoming' },
|
||||
{ label: 'Calendar', value: 'calendar', slot: 'calendar' },
|
||||
]"
|
||||
class="max-w-6xl mx-auto"
|
||||
>
|
||||
<template #upcoming>
|
||||
<div class="max-w-4xl mx-auto space-y-6 pt-8">
|
||||
<NuxtLink
|
||||
v-for="event in upcomingEvents"
|
||||
:key="event.id"
|
||||
:to="`/events/${event.slug || event.id}`"
|
||||
class="group flex items-start gap-4 py-2 hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<div class="flex-shrink-0 text-center">
|
||||
<div class="text-2xl font-bold text-stone-100">
|
||||
{{ event.start.getDate() }}
|
||||
</div>
|
||||
<div class="text-xs text-stone-400 uppercase">
|
||||
{{
|
||||
event.start.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-start gap-2 mb-1">
|
||||
<h3
|
||||
class="text-lg font-semibold text-stone-100 group-hover:text-blue-400 transition-colors"
|
||||
>
|
||||
{{ event.title }}
|
||||
</h3>
|
||||
<Icon
|
||||
v-if="event.membersOnly"
|
||||
name="heroicons:lock-closed"
|
||||
class="w-4 h-4 text-purple-500 flex-shrink-0 mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-stone-300 mb-2 line-clamp-2">
|
||||
{{ event.content }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-if="event.series?.isSeriesEvent"
|
||||
class="flex items-center gap-1 text-xs text-purple-600 dark:text-purple-400"
|
||||
>
|
||||
<div
|
||||
class="w-4 h-4 bg-purple-100 dark:bg-purple-900/30 rounded-full flex items-center justify-center text-xs font-bold"
|
||||
>
|
||||
{{ event.series.position }}
|
||||
</div>
|
||||
{{ event.series.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Icon
|
||||
name="heroicons:arrow-right"
|
||||
class="w-5 h-5 text-stone-400 group-hover:text-blue-400 group-hover:translate-x-1 transition-all flex-shrink-0 mt-1"
|
||||
/>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #calendar>
|
||||
<div class="pt-8">
|
||||
<ClientOnly>
|
||||
<div
|
||||
v-if="pending"
|
||||
class="min-h-[400px] bg-stone-700 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<div class="text-center">
|
||||
<p class="text-stone-200">Loading events...</p>
|
||||
</div>
|
||||
</div>
|
||||
<VueCal
|
||||
v-else
|
||||
:events="events"
|
||||
:time="false"
|
||||
active-view="month"
|
||||
class="custom-calendar"
|
||||
:disable-views="['years', 'year']"
|
||||
:hide-weekends="false"
|
||||
today-button
|
||||
events-on-month-view="short"
|
||||
:editable-events="{
|
||||
title: false,
|
||||
drag: false,
|
||||
resize: false,
|
||||
delete: false,
|
||||
create: false,
|
||||
}"
|
||||
@event-click="onEventClick"
|
||||
/>
|
||||
<template #fallback>
|
||||
<div
|
||||
class="min-h-[400px] bg-stone-700 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4"
|
||||
></div>
|
||||
<p class="text-stone-200">Loading calendar...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
</UTabs>
|
||||
</UContainer>
|
||||
</section>
|
||||
|
||||
<!-- Event Series -->
|
||||
<section v-if="activeSeries.length > 0" class="py-20 bg-purple-50 dark:bg-purple-900/20">
|
||||
<section
|
||||
v-if="activeSeries.length > 0"
|
||||
class="py-20 bg-stone-800 dark:bg-stone-900"
|
||||
>
|
||||
<UContainer>
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-8">
|
||||
<h2 class="text-3xl font-bold text-stone-100 mb-8">
|
||||
Active Event Series
|
||||
</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
Multi-part workshops and recurring events designed to deepen your knowledge and build community connections.
|
||||
<p class="text-stone-300 max-w-2xl mx-auto">
|
||||
Multi-part workshops and recurring events designed to deepen your
|
||||
knowledge and build community connections.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto"
|
||||
>
|
||||
<div
|
||||
v-for="series in activeSeries.slice(0, 6)"
|
||||
:key="series.id"
|
||||
class="bg-white dark:bg-gray-900 rounded-xl p-6 shadow-lg border border-gray-200 dark:border-gray-700"
|
||||
class="bg-stone-900 rounded-xl p-6 shadow-lg border border-stone-700"
|
||||
>
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div :class="[
|
||||
'inline-flex items-center px-3 py-1 rounded-full text-xs font-medium',
|
||||
getSeriesTypeBadgeClass(series.type)
|
||||
]">
|
||||
<div
|
||||
:class="[
|
||||
'inline-flex items-center px-3 py-1 rounded-full text-xs font-medium',
|
||||
getSeriesTypeBadgeClass(series.type),
|
||||
]"
|
||||
>
|
||||
{{ formatSeriesType(series.type) }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1 text-xs text-gray-500">
|
||||
<div class="flex items-center gap-1 text-xs text-stone-400">
|
||||
<Icon name="heroicons:calendar-days" class="w-4 h-4" />
|
||||
<span>{{ series.eventCount }} events</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
|
||||
<h3 class="text-lg font-semibold text-stone-100 mb-2">
|
||||
{{ series.title }}
|
||||
</h3>
|
||||
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
|
||||
|
||||
<p class="text-sm text-stone-300 mb-4 line-clamp-2">
|
||||
{{ series.description }}
|
||||
</p>
|
||||
|
||||
|
||||
<div class="space-y-2 mb-4">
|
||||
<div
|
||||
v-for="event in series.events.slice(0, 3)"
|
||||
<div
|
||||
v-for="event in series.events.slice(0, 3)"
|
||||
:key="event.id"
|
||||
class="flex items-center justify-between text-xs"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-6 h-6 bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400 rounded-full flex items-center justify-center text-xs font-medium">
|
||||
{{ event.series?.position || '?' }}
|
||||
<div
|
||||
class="w-6 h-6 bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400 rounded-full flex items-center justify-center text-xs font-medium"
|
||||
>
|
||||
{{ event.series?.position || "?" }}
|
||||
</div>
|
||||
<span class="text-gray-600 dark:text-gray-400 truncate">{{ event.title }}</span>
|
||||
<span class="text-stone-300 truncate">{{ event.title }}</span>
|
||||
</div>
|
||||
<span class="text-gray-500 dark:text-gray-500">
|
||||
<span class="text-stone-400">
|
||||
{{ formatEventDate(event.startDate) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="series.events.length > 3" class="text-xs text-gray-500 dark:text-gray-500 text-center pt-1">
|
||||
<div
|
||||
v-if="series.events.length > 3"
|
||||
class="text-xs text-stone-400 text-center pt-1"
|
||||
>
|
||||
+{{ series.events.length - 3 }} more events
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<div class="text-gray-500 dark:text-gray-500">
|
||||
<div class="text-stone-400">
|
||||
{{ formatDateRange(series.startDate, series.endDate) }}
|
||||
</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 dark:bg-green-900/30 dark:text-green-400' :
|
||||
series.status === 'upcoming' ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400' :
|
||||
'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400'
|
||||
]">
|
||||
<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 dark:bg-green-900/30 dark:text-green-400'
|
||||
: series.status === 'upcoming'
|
||||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
: 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400',
|
||||
]"
|
||||
>
|
||||
{{ series.status }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -144,237 +222,101 @@
|
|||
</UContainer>
|
||||
</section>
|
||||
|
||||
<!-- Upcoming Events -->
|
||||
<section class="py-20 bg-gray-50 dark:bg-gray-800">
|
||||
<UContainer>
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl font-bold text-blue-600 dark:text-blue-400 mb-8">
|
||||
Upcoming Events
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
|
||||
<NuxtLink
|
||||
v-for="event in upcomingEvents"
|
||||
:key="event.id"
|
||||
:to="`/events/${event.slug || event.id}`"
|
||||
class="group bg-white dark:bg-gray-900 rounded-xl overflow-hidden shadow-lg border border-gray-200 dark:border-gray-700 hover:border-blue-400 dark:hover:border-blue-600 transition-all hover:shadow-xl"
|
||||
>
|
||||
<!-- Feature Image -->
|
||||
<div v-if="event.featureImage?.url" class="aspect-video w-full overflow-hidden">
|
||||
<img
|
||||
:src="getImageUrl(event.featureImage)"
|
||||
:alt="event.featureImage.alt || event.title"
|
||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<!-- Series Badge -->
|
||||
<div v-if="event.series?.isSeriesEvent" class="mb-3">
|
||||
<div class="inline-flex items-center gap-1 text-xs font-medium text-purple-600 dark:text-purple-400">
|
||||
<div class="w-4 h-4 bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400 rounded-full flex items-center justify-center text-xs font-bold">
|
||||
{{ event.series.position }}
|
||||
</div>
|
||||
<Icon name="heroicons:squares-2x2" class="w-3 h-3" />
|
||||
{{ event.series.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div :class="[
|
||||
'inline-flex items-center px-3 py-1 rounded-full text-xs font-medium',
|
||||
event.class === 'event-community' ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400' :
|
||||
event.class === 'event-workshop' ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400' :
|
||||
event.class === 'event-social' ? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' :
|
||||
'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400'
|
||||
]">
|
||||
{{ event.class === 'event-community' ? 'Community' :
|
||||
event.class === 'event-workshop' ? 'Workshop' :
|
||||
event.class === 'event-social' ? 'Social' : 'Showcase' }}
|
||||
</div>
|
||||
<Icon v-if="event.membersOnly" name="heroicons:lock-closed" class="w-4 h-4 text-purple-500" />
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
|
||||
{{ event.title }}
|
||||
</h3>
|
||||
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
{{ event.content }}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center text-sm text-gray-500 dark:text-gray-500">
|
||||
<Icon name="heroicons:calendar" class="w-4 h-4 mr-1" />
|
||||
{{ formatEventDate(event.start) }}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800">
|
||||
<span class="inline-flex items-center text-sm font-medium text-blue-600 dark:text-blue-400 group-hover:translate-x-1 transition-transform">
|
||||
View Details
|
||||
<Icon name="heroicons:arrow-right" class="w-4 h-4 ml-1" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</UContainer>
|
||||
</section>
|
||||
|
||||
<!-- Attend Our Events -->
|
||||
<section class="py-20 bg-white dark:bg-gray-900">
|
||||
<section class="py-20 bg-stone-800 dark:bg-stone-900">
|
||||
<UContainer>
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-3xl font-bold text-blue-600 dark:text-blue-400 mb-8">
|
||||
<h2 class="text-3xl font-bold text-stone-100 mb-8">
|
||||
Attend Our Events
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-2xl p-8 border border-blue-200 dark:border-blue-800 mb-12">
|
||||
<div class="space-y-6 mb-8">
|
||||
<div class="h-2 bg-blue-500 rounded-full" />
|
||||
<div class="h-2 bg-blue-400 rounded-full w-5/6" />
|
||||
<div class="h-2 bg-blue-300 rounded-full w-2/3" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-stone-900 rounded-2xl p-8 border border-stone-700 mb-12"
|
||||
>
|
||||
<div class="prose prose-lg dark:prose-invert max-w-none">
|
||||
<p class="text-lg leading-relaxed text-gray-700 dark:text-gray-300 mb-6">
|
||||
Our events are designed to build community, share knowledge, and support developers exploring cooperative models. From informal networking sessions to structured workshops, there's something for everyone.
|
||||
<p class="text-lg leading-relaxed text-stone-200 mb-6">
|
||||
Our events are ,Lorem ipsum, dolor sit amet consectetur
|
||||
adipisicing elit. Quibusdam exercitationem delectus ab
|
||||
voluptates aspernatur, quia deleniti aut maxime, veniam
|
||||
accusantium non dolores saepe error, ipsam laudantium asperiores
|
||||
dolorum alias nulla!
|
||||
</p>
|
||||
|
||||
<p class="text-lg leading-relaxed text-gray-700 dark:text-gray-300 mb-6">
|
||||
Regular events include monthly community meetups, quarterly workshops on cooperative business structures, and seasonal social gatherings. We also host special events featuring guest speakers and collaborative project showcases.
|
||||
</p>
|
||||
|
||||
<p class="text-lg leading-relaxed text-gray-700 dark:text-gray-300">
|
||||
All events are welcoming to developers at any stage of their cooperative journey, from those just curious about alternative models to experienced co-op members sharing their insights.
|
||||
|
||||
<p class="text-lg leading-relaxed text-stone-200 mb-6">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
|
||||
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
|
||||
enim ad minim veniam, quis nostrud exercitation ullamco laboris
|
||||
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
|
||||
in reprehenderit in voluptate velit esse cillum dolore eu fugiat
|
||||
nulla pariatur. Excepteur sint occaecat cupidatat non proident,
|
||||
sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
|
||||
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
|
||||
enim ad minim veniam, quis nostrud exercitation ullamco laboris
|
||||
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
|
||||
in reprehenderit in voluptate velit esse cillum dolore eu fugiat
|
||||
nulla pariatur. Excepteur sint occaecat cupidatat non proident,
|
||||
sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded" />
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Monthly Meetups</h3>
|
||||
<div class="space-y-1 mb-3">
|
||||
<div class="h-1 bg-blue-500 rounded-full" />
|
||||
<div class="h-1 bg-blue-300 rounded-full w-3/4 mx-auto" />
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Casual networking and knowledge sharing sessions
|
||||
<h3 class="text-lg font-semibold text-stone-100 mb-2">
|
||||
Monthly Meetups
|
||||
</h3>
|
||||
|
||||
<p class="text-sm text-stone-300">
|
||||
Casual knowledge sharing sessions
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-emerald-100 dark:bg-emerald-900/30 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<div class="w-8 h-8 bg-emerald-500" style="clip-path: polygon(50% 0%, 0% 100%, 100% 100%)" />
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Workshops</h3>
|
||||
<div class="space-y-1 mb-3">
|
||||
<div class="h-1 bg-emerald-500 rounded-full" />
|
||||
<div class="h-1 bg-emerald-300 rounded-full w-5/6 mx-auto" />
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Hands-on learning about cooperative business models
|
||||
<h3 class="text-lg font-semibold text-stone-100 mb-2">
|
||||
Workshops
|
||||
</h3>
|
||||
|
||||
<p class="text-sm text-stone-300">
|
||||
Hands-on learning about cooperative and worker-centric business
|
||||
models
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-purple-100 dark:bg-purple-900/30 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<div class="w-8 h-8 bg-purple-500 rounded-full" />
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Social Events</h3>
|
||||
<div class="space-y-1 mb-3">
|
||||
<div class="h-1 bg-purple-500 rounded-full" />
|
||||
<div class="h-1 bg-purple-300 rounded-full w-2/3 mx-auto" />
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Community building and celebration gatherings
|
||||
<h3 class="text-lg font-semibold text-stone-100 mb-2">
|
||||
Social Events
|
||||
</h3>
|
||||
<p class="text-sm text-stone-300">
|
||||
Game nights, socials, and more
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UContainer>
|
||||
</section>
|
||||
|
||||
<!-- Event Highlights -->
|
||||
<section class="py-20 bg-gray-50 dark:bg-gray-800">
|
||||
<UContainer>
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-3xl font-bold text-blue-600 dark:text-blue-400 mb-8">
|
||||
Event Highlights
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 max-w-6xl mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-4">
|
||||
<div class="h-2 bg-blue-500 rounded-full" />
|
||||
<div class="h-2 bg-blue-400 rounded-full w-5/6" />
|
||||
<div class="h-2 bg-blue-300 rounded-full w-3/4" />
|
||||
<div class="h-2 bg-blue-200 rounded-full w-1/2" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Recent Highlights
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 leading-relaxed mb-4">
|
||||
Our latest workshop on "Building Sustainable Game Co-ops" brought together 50+ developers to explore practical strategies for transitioning to cooperative models.
|
||||
</p>
|
||||
<p class="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
The quarterly showcase featured three member studios presenting their games and sharing insights about democratic decision-making in creative projects.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Upcoming Features
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
Next month's event will include a panel discussion on funding cooperative studios, featuring successful co-op founders and supporting investors.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="w-full max-w-md h-64 bg-blue-100 dark:bg-blue-900/30 rounded-2xl border-2 border-dashed border-blue-300 dark:border-blue-700 flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-blue-200 dark:bg-blue-800 rounded-xl flex items-center justify-center mx-auto mb-4">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded" />
|
||||
</div>
|
||||
<p class="text-blue-600 dark:text-blue-400 font-medium">Event Photos</p>
|
||||
<p class="text-sm text-blue-500 dark:text-blue-500 mt-2">Coming Soon</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UContainer>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { VueCal } from 'vue-cal'
|
||||
import 'vue-cal/style.css'
|
||||
import { VueCal } from "vue-cal";
|
||||
import "vue-cal/style.css";
|
||||
|
||||
// Active tab state
|
||||
const activeTab = ref("upcoming");
|
||||
|
||||
// Fetch events from API
|
||||
const { data: eventsData, pending, error } = await useFetch('/api/events')
|
||||
const { data: eventsData, pending, error } = await useFetch("/api/events");
|
||||
// Fetch series from API
|
||||
const { data: seriesData } = await useFetch('/api/series')
|
||||
const { data: seriesData } = await useFetch("/api/series");
|
||||
|
||||
// Transform events for calendar display
|
||||
const events = computed(() => {
|
||||
if (!eventsData.value) return []
|
||||
|
||||
return eventsData.value.map(event => ({
|
||||
if (!eventsData.value) return [];
|
||||
|
||||
return eventsData.value.map((event) => ({
|
||||
id: event.id || event._id,
|
||||
slug: event.slug,
|
||||
start: new Date(event.startDate),
|
||||
|
|
@ -388,117 +330,128 @@ const events = computed(() => {
|
|||
registeredCount: event.registeredCount,
|
||||
maxAttendees: event.maxAttendees,
|
||||
featureImage: event.featureImage,
|
||||
series: event.series
|
||||
}))
|
||||
})
|
||||
series: event.series,
|
||||
}));
|
||||
});
|
||||
|
||||
// Get active event series
|
||||
const activeSeries = computed(() => {
|
||||
if (!seriesData.value) return []
|
||||
return seriesData.value.filter(series =>
|
||||
series.status === 'active' || series.isOngoing || series.isUpcoming
|
||||
)
|
||||
})
|
||||
if (!seriesData.value) return [];
|
||||
return seriesData.value.filter(
|
||||
(series) =>
|
||||
series.status === "active" || series.isOngoing || series.isUpcoming,
|
||||
);
|
||||
});
|
||||
|
||||
// Get upcoming events (future events)
|
||||
const upcomingEvents = computed(() => {
|
||||
const now = new Date()
|
||||
const now = new Date();
|
||||
return events.value
|
||||
.filter(event => event.start > now)
|
||||
.filter((event) => event.start > now)
|
||||
.sort((a, b) => a.start - b.start)
|
||||
.slice(0, 6) // Show max 6 upcoming events
|
||||
})
|
||||
.slice(0, 6); // Show max 6 upcoming events
|
||||
});
|
||||
|
||||
// Format event date for display
|
||||
const formatEventDate = (date) => {
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
}).format(date)
|
||||
}
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
// Get optimized Cloudinary image URL
|
||||
const getOptimizedImageUrl = (publicId, transformations) => {
|
||||
if (!publicId) return ''
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
return `https://res.cloudinary.com/${config.public.cloudinaryCloudName}/image/upload/${transformations}/f_auto,q_auto/${publicId}`
|
||||
}
|
||||
if (!publicId) return "";
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
return `https://res.cloudinary.com/${config.public.cloudinaryCloudName}/image/upload/${transformations}/f_auto,q_auto/${publicId}`;
|
||||
};
|
||||
|
||||
// Get image URL with fallback logic
|
||||
const getImageUrl = (featureImage) => {
|
||||
if (!featureImage) return ''
|
||||
|
||||
if (!featureImage) return "";
|
||||
|
||||
// If we have a direct URL, use it as primary (since seed data uses external URLs)
|
||||
if (featureImage.url) {
|
||||
return featureImage.url
|
||||
return featureImage.url;
|
||||
}
|
||||
|
||||
|
||||
// Fallback to Cloudinary if we have a publicId
|
||||
if (featureImage.publicId) {
|
||||
return getOptimizedImageUrl(featureImage.publicId, 'w_400,h_200,c_fill')
|
||||
return getOptimizedImageUrl(featureImage.publicId, "w_400,h_200,c_fill");
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
// Handle image loading errors
|
||||
const handleImageError = (event) => {
|
||||
console.warn('Image failed to load:', event.target.src)
|
||||
console.warn("Image failed to load:", event.target.src);
|
||||
// Optionally hide the image container or show a placeholder
|
||||
}
|
||||
};
|
||||
|
||||
// Handle calendar event click
|
||||
const onEventClick = (event) => {
|
||||
if (event.id) {
|
||||
navigateTo(`/events/${event.slug || event.id}`)
|
||||
navigateTo(`/events/${event.slug || event.id}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Series 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 dark:bg-emerald-900/30 dark:text-emerald-400',
|
||||
'recurring_meetup': 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
||||
'multi_day': 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
|
||||
'course': 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',
|
||||
'tournament': 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||||
}
|
||||
return classes[type] || 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400'
|
||||
}
|
||||
workshop_series:
|
||||
"bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400",
|
||||
recurring_meetup:
|
||||
"bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
|
||||
multi_day:
|
||||
"bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400",
|
||||
course:
|
||||
"bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400",
|
||||
tournament: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400",
|
||||
};
|
||||
return (
|
||||
classes[type] ||
|
||||
"bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400"
|
||||
);
|
||||
};
|
||||
|
||||
const formatDateRange = (startDate, endDate) => {
|
||||
if (!startDate || !endDate) return 'No dates'
|
||||
|
||||
const start = new Date(startDate)
|
||||
const end = new Date(endDate)
|
||||
|
||||
const startMonth = start.toLocaleDateString('en-US', { month: 'short' })
|
||||
const endMonth = end.toLocaleDateString('en-US', { month: 'short' })
|
||||
const startDay = start.getDate()
|
||||
const endDay = end.getDate()
|
||||
const year = end.getFullYear()
|
||||
|
||||
if (start.getMonth() === end.getMonth() && start.getFullYear() === end.getFullYear()) {
|
||||
return `${startMonth} ${startDay}-${endDay}, ${year}`
|
||||
if (!startDate || !endDate) return "No dates";
|
||||
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
|
||||
const startMonth = start.toLocaleDateString("en-US", { month: "short" });
|
||||
const endMonth = end.toLocaleDateString("en-US", { month: "short" });
|
||||
const startDay = start.getDate();
|
||||
const endDay = end.getDate();
|
||||
const year = end.getFullYear();
|
||||
|
||||
if (
|
||||
start.getMonth() === end.getMonth() &&
|
||||
start.getFullYear() === end.getFullYear()
|
||||
) {
|
||||
return `${startMonth} ${startDay}-${endDay}, ${year}`;
|
||||
} else if (start.getFullYear() === end.getFullYear()) {
|
||||
return `${startMonth} ${startDay} - ${endMonth} ${endDay}, ${year}`
|
||||
return `${startMonth} ${startDay} - ${endMonth} ${endDay}, ${year}`;
|
||||
} else {
|
||||
return `${formatEventDate(startDate)} - ${formatEventDate(endDate)}`
|
||||
return `${formatEventDate(startDate)} - ${formatEventDate(endDate)}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -511,19 +464,127 @@ const formatDateRange = (startDate, endDate) => {
|
|||
|
||||
/* Custom calendar styling to match the site theme */
|
||||
.custom-calendar {
|
||||
--vuecal-primary-color: #3b82f6;
|
||||
--vuecal-text-color: #374151;
|
||||
--vuecal-border-color: #e5e7eb;
|
||||
--vuecal-header-color: #f9fafb;
|
||||
--vuecal-today-color: #dbeafe;
|
||||
--vuecal-primary-color: #fff;
|
||||
--vuecal-text-color: #e7e5e4;
|
||||
--vuecal-border-color: #57534e;
|
||||
--vuecal-header-color: #1c1917;
|
||||
--vuecal-today-color: #292524;
|
||||
background-color: #292524;
|
||||
}
|
||||
|
||||
.dark .custom-calendar {
|
||||
--vuecal-primary-color: #60a5fa;
|
||||
--vuecal-text-color: #d1d5db;
|
||||
--vuecal-border-color: #4b5563;
|
||||
--vuecal-header-color: #374151;
|
||||
--vuecal-today-color: #1e3a8a;
|
||||
.custom-calendar :deep(.vuecal__bg) {
|
||||
background-color: #292524;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__header) {
|
||||
background-color: #1c1917;
|
||||
border-bottom: 1px solid #57534e;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__title-bar) {
|
||||
background-color: #1c1917;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__title) {
|
||||
color: #e7e5e4;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__weekdays-headings) {
|
||||
background-color: #1c1917;
|
||||
border-bottom: 1px solid #57534e;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__heading) {
|
||||
color: #a8a29e;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__cell) {
|
||||
background-color: #292524;
|
||||
border-color: #57534e;
|
||||
color: #e7e5e4;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__cell:hover) {
|
||||
background-color: #44403c;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__cell-content) {
|
||||
color: #e7e5e4;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__cell--today) {
|
||||
background-color: #44403c;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__cell--out-of-scope) {
|
||||
background-color: #1c1917;
|
||||
color: #78716c;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__arrow) {
|
||||
color: #a8a29e;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__arrow:hover) {
|
||||
background-color: #44403c;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__today-btn) {
|
||||
background-color: #44403c;
|
||||
color: white;
|
||||
border: 1px solid #78716c;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__today-btn:hover) {
|
||||
background-color: #57534e;
|
||||
border-color: #a8a29e;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__view-btn),
|
||||
.custom-calendar :deep(button[class*="view"]) {
|
||||
background-color: #44403c !important;
|
||||
color: #ffffff !important;
|
||||
border: 1px solid #78716c !important;
|
||||
font-weight: 600 !important;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__view-btn:hover),
|
||||
.custom-calendar :deep(button[class*="view"]:hover) {
|
||||
background-color: #57534e !important;
|
||||
border-color: #a8a29e !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__view-btn--active),
|
||||
.custom-calendar :deep(button[class*="view"][class*="active"]) {
|
||||
background-color: #0c0a09 !important;
|
||||
color: #ffffff !important;
|
||||
border-color: #a8a29e !important;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__view-btn--active:hover),
|
||||
.custom-calendar :deep(button[class*="view"][class*="active"]:hover) {
|
||||
background-color: #1c1917 !important;
|
||||
border-color: #d6d3d1 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__title-bar button) {
|
||||
color: #ffffff !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__title-bar .default-view-btn) {
|
||||
background-color: #44403c !important;
|
||||
color: #ffffff !important;
|
||||
border: 1px solid #78716c !important;
|
||||
}
|
||||
|
||||
.custom-calendar :deep(.vuecal__title-bar .default-view-btn.active) {
|
||||
background-color: #0c0a09 !important;
|
||||
border-color: #a8a29e !important;
|
||||
}
|
||||
|
||||
/* Event type styling */
|
||||
|
|
@ -562,4 +623,4 @@ const formatDateRange = (startDate, endDate) => {
|
|||
color: var(--vuecal-primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue