Adding features

This commit is contained in:
Jennie Robinson Faber 2025-10-05 16:15:09 +01:00
parent 600fef2b7c
commit 2b55ca4104
75 changed files with 9796 additions and 2759 deletions

View file

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