Many an update!

This commit is contained in:
Jennie Robinson Faber 2025-12-01 15:26:42 +00:00
parent 85195d6c7a
commit d588c49946
35 changed files with 3528 additions and 1142 deletions

View file

@ -13,13 +13,115 @@
<UTabs
v-model="activeTab"
:items="[
{ label: 'Upcoming Events', value: 'upcoming', slot: 'upcoming' },
{ label: 'What\'s On', 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">
<!-- Search and Filter Controls -->
<div class="pt-8">
<div class="max-w-4xl mx-auto mb-8">
<div class="flex flex-col gap-4">
<!-- Search Input -->
<div
class="flex flex-row flex-wrap gap-4 items-center justify-center"
>
<UInput
v-model="searchQuery"
placeholder="Search events..."
autocomplete="off"
size="xl"
class="rounded-md shadow-sm text-lg w-full md:w-2/3"
:ui="{ icon: { trailing: { pointer: '' } } }"
>
<template #trailing>
<UButton
v-show="searchQuery"
icon="i-heroicons-x-mark-20-solid"
color="gray"
variant="ghost"
@click="searchQuery = ''"
class="mr-1"
/>
</template>
</UInput>
<!-- Past Events Toggle -->
<div
class="flex flex-col items-center md:items-start gap-2"
>
<label class="text-sm font-medium text-ghost-200">
Show past events?
</label>
<USwitch v-model="includePastEvents" />
</div>
</div>
<!-- Category Filter -->
<div class="flex justify-center">
<UPopover
:ui="{ base: 'overflow-visible' }"
:popper="{
placement: 'bottom-end',
strategy: 'absolute',
}"
>
<UButton
icon="i-heroicons-adjustments-vertical-20-solid"
variant="outline"
color="primary"
size="sm"
class="rounded-md shadow-sm"
>
Filter Events
</UButton>
<template #panel="{ close }">
<div
class="bg-ghost-800 dark:bg-ghost-900 p-6 rounded-md shadow-lg w-80 space-y-4"
>
<div class="space-y-2">
<label class="text-sm font-medium text-ghost-200">
Filter by category
</label>
<USelectMenu
v-model="selectedCategories"
:options="eventCategories"
option-attribute="name"
multiple
value-attribute="id"
placeholder="Select categories..."
:ui="{
base: 'overflow-visible',
menu: { maxHeight: '200px', overflowY: 'auto' },
popper: { strategy: 'absolute' },
}"
/>
</div>
</div>
</template>
</UPopover>
</div>
</div>
<!-- Search Status Message -->
<div v-if="isSearching" class="mt-6 text-center">
<p class="text-ghost-300 text-sm">
{{
filteredEvents.length > 0
? `Found ${filteredEvents.length} event${
filteredEvents.length !== 1 ? "s" : ""
}`
: "No events found matching your search"
}}
</p>
</div>
</div>
</div>
<!-- Events List -->
<div class="max-w-4xl mx-auto space-y-6 pb-8">
<NuxtLink
v-for="event in upcomingEvents"
:key="event.id"
@ -76,48 +178,49 @@
</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="pt-8 pb-8">
<div class="max-w-6xl mx-auto" id="event-calendar">
<ClientOnly>
<div
class="min-h-[400px] bg-ghost-700 rounded-xl flex items-center justify-center"
v-if="pending"
class="min-h-[400px] bg-ghost-800 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"
class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"
></div>
<p class="text-ghost-200">Loading calendar...</p>
</div>
</div>
</template>
</ClientOnly>
<div v-else style="min-height: 600px">
<VueCal
ref="vuecal"
:events="calendarEvents"
:time="false"
active-view="month"
class="ghost-calendar"
:disable-views="['years', 'year']"
:hide-view-selector="false"
events-on-month-view="short"
today-button
@event-click="onEventClick"
>
</VueCal>
</div>
<template #fallback>
<div
class="min-h-[400px] bg-ghost-800 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-primary mx-auto mb-4"
></div>
<p class="text-ghost-200">Loading calendar...</p>
</div>
</div>
</template>
</ClientOnly>
</div>
</div>
</template>
</UTabs>
@ -183,7 +286,7 @@
<div class="series-list-item__events space-y-2 mb-4">
<div
v-for="(event, index) in series.events.slice(0, 3)"
:key="event.id"
:key="index"
class="series-list-item__event flex items-center justify-between text-xs"
>
<div class="flex items-center gap-2">
@ -234,28 +337,16 @@
>
<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!
Our events bring together game developers, founders, and practitioners
who are building more equitable workplaces. From hands-on workshops
about governance and finance to casual co-working sessions and game nights,
there's something for every stage of your journey.
</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.
All events are designed to be accessible, with most offered free to members
and sliding-scale pricing for non-members. Can't make it live?
Many sessions are recorded and shared in our resource library.
</p>
</div>
</div>
@ -301,19 +392,64 @@
import { VueCal } from "vue-cal";
import "vue-cal/style.css";
// Composables
const { formatDateRange, formatDate, isPastDate } = useEventDateUtils();
const {
searchQuery,
includePastEvents,
searchResults,
selectedCategories,
isSearching,
saveSearchState,
loadSearchState,
clearSearch: clearSearchFilters,
} = useCalendarSearch();
// Active tab state
const activeTab = ref("upcoming");
// Hover state for calendar cells
const hoveredCells = ref({});
const handleMouseEnter = (date) => {
hoveredCells.value[date] = true;
};
const handleMouseLeave = (date) => {
hoveredCells.value[date] = false;
};
const isHovered = (date) => {
return hoveredCells.value[date] || false;
};
// Fetch events from API
const { data: eventsData, pending, error } = await useFetch("/api/events");
// Fetch series from API
const { data: seriesData } = await useFetch("/api/series");
// Event categories for filtering
const eventCategories = computed(() => {
if (!eventsData.value) return [];
const categories = new Set();
eventsData.value.forEach((event) => {
if (event.eventType) {
categories.add(event.eventType);
}
});
return Array.from(categories).map((type) => ({
id: type,
name: type.charAt(0).toUpperCase() + type.slice(1),
}));
});
// Transform events for calendar display
const events = computed(() => {
if (!eventsData.value) return [];
if (!eventsData.value) {
return [];
}
return eventsData.value.map((event) => ({
const transformed = eventsData.value.map((event) => ({
id: event.id || event._id,
slug: event.slug,
start: new Date(event.startDate),
@ -329,6 +465,71 @@ const events = computed(() => {
featureImage: event.featureImage,
series: event.series,
}));
return transformed;
});
// Helper function to format date for VueCal
// When time is disabled, VueCal expects just 'YYYY-MM-DD' format
const formatDateForVueCal = (date) => {
const d = date instanceof Date ? date : new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
// Transform events for calendar view
const calendarEvents = computed(() => {
const filtered = events.value
.filter((event) => {
// Filter by past events setting
if (!includePastEvents.value && isPastDate(event.start)) {
return false;
}
return true;
})
.map((event) => {
// VueCal expects Date objects when time is disabled
// Only pass title (no content/description) for clean display
return {
start: event.start, // Use Date object directly
end: event.end, // Use Date object directly
title: event.title, // Only title, no description
class: event.class,
};
});
return filtered;
});
// Filter events based on search query and selected categories
const filteredEvents = computed(() => {
return events.value.filter((event) => {
// Filter by past events setting
if (!includePastEvents.value && isPastDate(event.start)) {
return false;
}
// Filter by search query
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase();
const matchesTitle = event.title.toLowerCase().includes(query);
const matchesDescription =
event.content?.toLowerCase().includes(query) || false;
if (!matchesTitle && !matchesDescription) {
return false;
}
}
// Filter by selected categories
if (selectedCategories.value.length > 0) {
if (!selectedCategories.value.includes(event.eventType)) {
return false;
}
}
return true;
});
});
// Get active event series
@ -340,13 +541,13 @@ const activeSeries = computed(() => {
);
});
// Get upcoming events (future events)
// Get upcoming events (future events, respecting filters)
const upcomingEvents = computed(() => {
const now = new Date();
return events.value
return filteredEvents.value
.filter((event) => event.start > now)
.sort((a, b) => a.start - b.start)
.slice(0, 6); // Show max 6 upcoming events
.slice(0, 10); // Show max 10 upcoming events
});
// Format event date for display
@ -426,30 +627,6 @@ const getSeriesTypeBadgeClass = (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>
@ -460,8 +637,199 @@ const formatDateRange = (startDate, endDate) => {
overflow: hidden;
}
/* Custom calendar styling to match the site theme */
.custom-calendar {
/* Calendar styling based on tranzac design principles */
.ghost-calendar :deep(.vuecal__event) {
border-radius: 0.5rem;
border: 2px solid #292524;
transform: translateZ(0);
transition: transform 300ms;
}
.ghost-calendar :deep(.vuecal__event:hover) {
transform: scale(1.05);
}
.ghost-calendar :deep(.vuecal__event-title) {
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ghost-calendar :deep(.vuecal__event-time) {
display: none;
}
.ghost-calendar :deep(.vuecal__event.event-community) {
background-color: #2563eb;
color: #f5f5f4;
border-color: #1d4ed8;
}
.ghost-calendar :deep(.vuecal__event.event-workshop) {
background-color: #059669;
color: #f5f5f4;
border-color: #047857;
}
.ghost-calendar :deep(.vuecal__event.event-social) {
background-color: #7c3aed;
color: #f5f5f4;
border-color: #6d28d9;
}
.ghost-calendar :deep(.vuecal__event.event-showcase) {
background-color: #d97706;
color: #f5f5f4;
border-color: #b45309;
}
#event-calendar {
.vuecal__cell-events-count {
position: absolute;
top: 0.5rem;
right: 0.25rem;
left: auto;
background-color: transparent;
color: #f5f5f4;
font-weight: 700;
font-size: 1.25rem;
}
.month-view .vuecal__cell--today,
.vuecal__cell--today {
background-color: rgba(59, 130, 246, 0.15);
color: #f5f5f4;
border: 2px solid #3b82f6 !important;
.day-of-month {
display: flex;
align-items: center;
justify-content: center;
}
.day-of-month::before {
content: "Today";
text-transform: uppercase;
font-size: 0.75rem;
margin-right: 0.5rem;
display: none;
color: #3b82f6;
font-weight: 600;
}
@media (min-width: 768px) {
.day-of-month::before {
display: block;
}
}
}
.vuecal__cell--selected {
background-color: transparent;
}
.past-date {
background-color: rgba(120, 113, 108, 0.1);
color: #78716c;
}
.vuecal__cell--has-events {
}
.vuecal__cell--disabled {
}
.vuecal__cell-events {
}
.vuecal__event {
border-radius: 0.5rem;
border: 2px solid #3a3a3a;
transform: translateZ(0);
transition: transform 300ms;
&:hover {
transform: scale(1.05);
}
&.vuecal__event--selected {
background-color: #2a2a2a;
}
.vuecal__cell--today {
background-color: transparent;
}
.vuecal__event-title {
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #f5f5f4;
}
.vuecal__event-time {
font-weight: 400;
color: #a8a29e;
}
&.event-community {
background-color: #2563eb;
color: #f5f5f4;
border-color: #1d4ed8;
}
&.event-workshop {
background-color: #059669;
color: #f5f5f4;
border-color: #047857;
}
&.event-social {
background-color: #7c3aed;
color: #f5f5f4;
border-color: #6d28d9;
}
&.event-showcase {
background-color: #d97706;
color: #f5f5f4;
border-color: #b45309;
}
}
.vuecal__cell.vuecal__cell--out-of-scope {
pointer-events: none;
cursor: not-allowed;
border: none;
}
.outOfScope {
height: 100%;
width: 100%;
position: absolute;
background-color: rgba(0, 0, 0, 0.5);
pointer-events: none;
cursor: not-allowed;
}
:not(.vuecal__cell--out-of-scope) .vuecal__cell {
border: 1px solid #3a3a3a;
}
.vuecal__cell:before {
border: none;
}
.vuecal__title-bar {
margin-top: 1rem;
margin-bottom: 1rem;
}
}
/* Ghost calendar theme */
.ghost-calendar {
--vuecal-primary-color: #fff;
--vuecal-text-color: #e7e5e4;
--vuecal-border-color: #57534e;
@ -470,142 +838,124 @@ const formatDateRange = (startDate, endDate) => {
background-color: #292524;
}
.custom-calendar :deep(.vuecal__bg) {
.ghost-calendar :deep(.vuecal__bg) {
background-color: #292524;
}
.custom-calendar :deep(.vuecal__header) {
.ghost-calendar :deep(.vuecal__header) {
background-color: #1c1917;
border-bottom: 1px solid #57534e;
}
.custom-calendar :deep(.vuecal__title-bar) {
.ghost-calendar :deep(.vuecal__title-bar) {
background-color: #1c1917;
}
.custom-calendar :deep(.vuecal__title) {
.ghost-calendar :deep(.vuecal__title) {
color: #e7e5e4;
font-weight: 600;
}
.custom-calendar :deep(.vuecal__weekdays-headings) {
.ghost-calendar :deep(.vuecal__weekdays-headings) {
background-color: #1c1917;
border-bottom: 1px solid #57534e;
}
.custom-calendar :deep(.vuecal__heading) {
.ghost-calendar :deep(.vuecal__heading) {
color: #a8a29e;
font-weight: 500;
}
.custom-calendar :deep(.vuecal__cell) {
.ghost-calendar :deep(.vuecal__cell) {
background-color: #292524;
border-color: #57534e;
color: #e7e5e4;
}
.custom-calendar :deep(.vuecal__cell:hover) {
.ghost-calendar :deep(.vuecal__cell:hover) {
background-color: #44403c;
border-color: #78716c;
}
.custom-calendar :deep(.vuecal__cell-content) {
.ghost-calendar :deep(.vuecal__cell-content) {
color: #e7e5e4;
}
.custom-calendar :deep(.vuecal__cell--today) {
background-color: #44403c;
.ghost-calendar :deep(.vuecal__cell--today) {
background-color: rgba(59, 130, 246, 0.1);
border: 2px solid #3b82f6;
}
.custom-calendar :deep(.vuecal__cell--out-of-scope) {
.ghost-calendar :deep(.vuecal__cell--out-of-scope) {
background-color: #1c1917;
color: #78716c;
}
.custom-calendar :deep(.vuecal__arrow) {
.ghost-calendar :deep(.vuecal__arrow) {
color: #a8a29e;
}
.custom-calendar :deep(.vuecal__arrow:hover) {
.ghost-calendar :deep(.vuecal__arrow:hover) {
background-color: #44403c;
}
.custom-calendar :deep(.vuecal__today-btn) {
.ghost-calendar :deep(.vuecal__today-btn) {
background-color: #44403c;
color: white;
color: #fff;
border: 1px solid #78716c;
font-weight: 600;
}
.custom-calendar :deep(.vuecal__today-btn:hover) {
.ghost-calendar :deep(.vuecal__today-btn:hover) {
background-color: #57534e;
border-color: #a8a29e;
}
.custom-calendar :deep(.vuecal__view-btn),
.custom-calendar :deep(button[class*="view"]) {
.ghost-calendar :deep(.vuecal__view-btn),
.ghost-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) {
.ghost-calendar :deep(.vuecal__view-btn:hover),
.ghost-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"]) {
.ghost-calendar :deep(.vuecal__view-btn--active),
.ghost-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) {
.ghost-calendar :deep(.vuecal__view-btn--active:hover),
.ghost-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) {
.ghost-calendar :deep(.vuecal__title-bar button) {
color: #ffffff !important;
font-weight: 600 !important;
}
.custom-calendar :deep(.vuecal__title-bar .default-view-btn) {
.ghost-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) {
.ghost-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;