626 lines
19 KiB
Vue
626 lines
19 KiB
Vue
<template>
|
|
<div>
|
|
<!-- Page Header -->
|
|
<PageHeader
|
|
title="Events"
|
|
subtitle="Join our community events, workshops, and gatherings"
|
|
size="large"
|
|
/>
|
|
|
|
<!-- Events Section with Tabs -->
|
|
<section class="py-20 bg-ghost-900 dark:bg-ghost-950">
|
|
<UContainer>
|
|
<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-ghost-100">
|
|
{{ event.start.getDate() }}
|
|
</div>
|
|
<div class="text-xs text-ghost-400 uppercase">
|
|
{{
|
|
event.start.toLocaleDateString("en-US", {
|
|
month: "short",
|
|
})
|
|
}}
|
|
</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-ghost-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-ghost-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-ghost-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-ghost-700 rounded-xl flex items-center justify-center"
|
|
>
|
|
<div class="text-center">
|
|
<p class="text-ghost-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-ghost-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-ghost-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-ghost-800 dark:bg-ghost-900"
|
|
>
|
|
<UContainer>
|
|
<div class="text-center mb-12">
|
|
<h2 class="text-3xl font-bold text-ghost-100 mb-8">
|
|
Active Event Series
|
|
</h2>
|
|
<p class="text-ghost-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
|
|
v-for="series in activeSeries.slice(0, 6)"
|
|
:key="series.id"
|
|
class="bg-ghost-900 rounded-xl p-6 shadow-lg border border-ghost-700"
|
|
>
|
|
<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),
|
|
]"
|
|
>
|
|
{{ formatSeriesType(series.type) }}
|
|
</div>
|
|
<div class="flex items-center gap-1 text-xs text-ghost-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-ghost-100 mb-2">
|
|
{{ series.title }}
|
|
</h3>
|
|
|
|
<p class="text-sm text-ghost-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)"
|
|
: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>
|
|
<span class="text-ghost-300 truncate">{{ event.title }}</span>
|
|
</div>
|
|
<span class="text-ghost-400">
|
|
{{ formatEventDate(event.startDate) }}
|
|
</span>
|
|
</div>
|
|
<div
|
|
v-if="series.events.length > 3"
|
|
class="text-xs text-ghost-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-ghost-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',
|
|
]"
|
|
>
|
|
{{ series.status }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</UContainer>
|
|
</section>
|
|
|
|
<!-- Attend Our Events -->
|
|
<section class="py-20 bg-ghost-800 dark:bg-ghost-900">
|
|
<UContainer>
|
|
<div class="text-center mb-16">
|
|
<h2 class="text-3xl font-bold text-ghost-100 mb-8">
|
|
Attend Our Events
|
|
</h2>
|
|
</div>
|
|
|
|
<div class="max-w-4xl mx-auto">
|
|
<div
|
|
class="bg-ghost-900 rounded-2xl p-8 border border-ghost-700 mb-12"
|
|
>
|
|
<div class="prose prose-lg dark:prose-invert max-w-none">
|
|
<p class="text-lg leading-relaxed text-ghost-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-ghost-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">
|
|
<h3 class="text-lg font-semibold text-ghost-100 mb-2">
|
|
Monthly Meetups
|
|
</h3>
|
|
|
|
<p class="text-sm text-ghost-300">
|
|
Casual knowledge sharing sessions
|
|
</p>
|
|
</div>
|
|
|
|
<div class="text-center">
|
|
<h3 class="text-lg font-semibold text-ghost-100 mb-2">
|
|
Workshops
|
|
</h3>
|
|
|
|
<p class="text-sm text-ghost-300">
|
|
Hands-on learning about cooperative and worker-centric business
|
|
models
|
|
</p>
|
|
</div>
|
|
|
|
<div class="text-center">
|
|
<h3 class="text-lg font-semibold text-ghost-100 mb-2">
|
|
Social Events
|
|
</h3>
|
|
<p class="text-sm text-ghost-300">
|
|
Game nights, socials, and more
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</UContainer>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
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");
|
|
// Fetch series from API
|
|
const { data: seriesData } = await useFetch("/api/series");
|
|
|
|
// Transform events for calendar display
|
|
const events = computed(() => {
|
|
if (!eventsData.value) return [];
|
|
|
|
return eventsData.value.map((event) => ({
|
|
id: event.id || event._id,
|
|
slug: event.slug,
|
|
start: new Date(event.startDate),
|
|
end: new Date(event.endDate),
|
|
title: event.title,
|
|
content: event.description,
|
|
class: `event-${event.eventType}`,
|
|
membersOnly: event.membersOnly,
|
|
eventType: event.eventType,
|
|
location: event.location,
|
|
registeredCount: event.registeredCount,
|
|
maxAttendees: event.maxAttendees,
|
|
featureImage: event.featureImage,
|
|
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,
|
|
);
|
|
});
|
|
|
|
// Get upcoming events (future events)
|
|
const upcomingEvents = computed(() => {
|
|
const now = new Date();
|
|
return events.value
|
|
.filter((event) => event.start > now)
|
|
.sort((a, b) => a.start - b.start)
|
|
.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);
|
|
};
|
|
|
|
// 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}`;
|
|
};
|
|
|
|
// Get image URL with fallback logic
|
|
const getImageUrl = (featureImage) => {
|
|
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;
|
|
}
|
|
|
|
// Fallback to Cloudinary if we have a publicId
|
|
if (featureImage.publicId) {
|
|
return getOptimizedImageUrl(featureImage.publicId, "w_400,h_200,c_fill");
|
|
}
|
|
|
|
return "";
|
|
};
|
|
|
|
// Handle image loading errors
|
|
const handleImageError = (event) => {
|
|
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}`);
|
|
}
|
|
};
|
|
|
|
// 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;
|
|
};
|
|
|
|
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"
|
|
);
|
|
};
|
|
|
|
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}`;
|
|
} else if (start.getFullYear() === end.getFullYear()) {
|
|
return `${startMonth} ${startDay} - ${endMonth} ${endDay}, ${year}`;
|
|
} else {
|
|
return `${formatEventDate(startDate)} - ${formatEventDate(endDate)}`;
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.line-clamp-2 {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Custom calendar styling to match the site theme */
|
|
.custom-calendar {
|
|
--vuecal-primary-color: #fff;
|
|
--vuecal-text-color: #e7e5e4;
|
|
--vuecal-border-color: #57534e;
|
|
--vuecal-header-color: #1c1917;
|
|
--vuecal-today-color: #292524;
|
|
background-color: #292524;
|
|
}
|
|
|
|
.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 */
|
|
.vuecal__event.event-community {
|
|
background-color: #3b82f6;
|
|
border-color: #2563eb;
|
|
}
|
|
|
|
.vuecal__event.event-workshop {
|
|
background-color: #10b981;
|
|
border-color: #059669;
|
|
}
|
|
|
|
.vuecal__event.event-social {
|
|
background-color: #8b5cf6;
|
|
border-color: #7c3aed;
|
|
}
|
|
|
|
.vuecal__event.event-showcase {
|
|
background-color: #f59e0b;
|
|
border-color: #d97706;
|
|
}
|
|
|
|
/* Responsive calendar */
|
|
.vuecal {
|
|
border-radius: 0.75rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.vuecal__header {
|
|
background-color: var(--vuecal-header-color);
|
|
color: var(--vuecal-text-color);
|
|
}
|
|
|
|
.vuecal__title {
|
|
color: var(--vuecal-primary-color);
|
|
font-weight: 600;
|
|
}
|
|
</style>
|