Many an update!
This commit is contained in:
parent
85195d6c7a
commit
d588c49946
35 changed files with 3528 additions and 1142 deletions
|
|
@ -79,7 +79,7 @@
|
|||
<div class="max-w-4xl mx-auto">
|
||||
<!-- Event Meta Info -->
|
||||
<div class="mb-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div>
|
||||
<p class="text-sm text-ghost-400">Date</p>
|
||||
<p class="font-semibold text-ghost-100">
|
||||
|
|
@ -100,6 +100,20 @@
|
|||
{{ event.location }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm text-ghost-400">Calendar</p>
|
||||
<UButton
|
||||
:href="`/api/events/${route.params.id}/calendar`"
|
||||
download
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="mt-1"
|
||||
icon="i-heroicons-calendar-days"
|
||||
>
|
||||
Add to Calendar
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -270,28 +284,64 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logged In - Can Register -->
|
||||
<div
|
||||
v-else-if="memberData && (!event.membersOnly || isMember)"
|
||||
class="text-center"
|
||||
>
|
||||
<p class="text-lg text-ghost-200 mb-6">
|
||||
You are logged in, {{ memberData.name }}.
|
||||
</p>
|
||||
<UButton
|
||||
color="primary"
|
||||
size="xl"
|
||||
@click="handleRegistration"
|
||||
:loading="isRegistering"
|
||||
class="px-12 py-4"
|
||||
<!-- Member Status Check (pending payment, suspended, cancelled) -->
|
||||
<div v-else-if="memberData && !canRSVP" class="text-center">
|
||||
<div
|
||||
:class="[
|
||||
'p-6 rounded-lg border mb-6',
|
||||
statusConfig.bgColor,
|
||||
statusConfig.borderColor,
|
||||
]"
|
||||
>
|
||||
{{ isRegistering ? "Registering..." : "Register Now" }}
|
||||
</UButton>
|
||||
<Icon
|
||||
:name="statusConfig.icon"
|
||||
:class="['w-8 h-8 mx-auto mb-3', statusConfig.textColor]"
|
||||
/>
|
||||
<p
|
||||
:class="[
|
||||
'font-semibold text-lg mb-2',
|
||||
statusConfig.textColor,
|
||||
]"
|
||||
>
|
||||
{{ statusConfig.label }}
|
||||
</p>
|
||||
<p :class="['mb-4', statusConfig.textColor]">
|
||||
{{ getRSVPMessage }}
|
||||
</p>
|
||||
<UButton
|
||||
v-if="isPendingPayment"
|
||||
color="orange"
|
||||
size="lg"
|
||||
class="px-8"
|
||||
:loading="isProcessingPayment"
|
||||
@click="completePayment"
|
||||
>
|
||||
{{
|
||||
isProcessingPayment ? "Processing..." : "Complete Payment"
|
||||
}}
|
||||
</UButton>
|
||||
<NuxtLink
|
||||
v-else-if="isCancelled"
|
||||
to="/member/profile#account"
|
||||
>
|
||||
<UButton color="blue" size="lg" class="px-8">
|
||||
Reactivate Membership
|
||||
</UButton>
|
||||
</NuxtLink>
|
||||
<a
|
||||
v-else-if="isSuspended"
|
||||
href="mailto:support@ghostguild.org"
|
||||
>
|
||||
<UButton color="gray" size="lg" class="px-8">
|
||||
Contact Support
|
||||
</UButton>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Member Gate Warning -->
|
||||
<!-- Member Gate Warning (members-only locked event) -->
|
||||
<div
|
||||
v-else-if="event.membersOnly && !isMember"
|
||||
v-else-if="event.membersOnly && memberData && !isMember"
|
||||
class="text-center"
|
||||
>
|
||||
<div
|
||||
|
|
@ -312,6 +362,25 @@
|
|||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Logged In - Can Register -->
|
||||
<div
|
||||
v-else-if="memberData && (!event.membersOnly || isMember)"
|
||||
class="text-center"
|
||||
>
|
||||
<p class="text-lg text-ghost-200 mb-6">
|
||||
You are logged in, {{ memberData.name }}.
|
||||
</p>
|
||||
<UButton
|
||||
color="primary"
|
||||
size="xl"
|
||||
@click="handleRegistration"
|
||||
:loading="isRegistering"
|
||||
class="px-12 py-4"
|
||||
>
|
||||
{{ isRegistering ? "Registering..." : "Register Now" }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- Not Logged In - Show Registration Form -->
|
||||
<div v-else>
|
||||
<h3 class="text-xl font-bold text-ghost-100 mb-6">
|
||||
|
|
@ -403,6 +472,64 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Waitlist Section -->
|
||||
<div
|
||||
v-if="event.tickets?.waitlist?.enabled && isEventFull"
|
||||
class="mt-6 pt-6 border-t border-ghost-700"
|
||||
>
|
||||
<!-- Already on Waitlist -->
|
||||
<div v-if="isOnWaitlist" class="text-center">
|
||||
<div class="p-4 bg-amber-900/20 rounded-lg border border-amber-800 mb-4">
|
||||
<p class="font-semibold text-amber-300">
|
||||
You're on the waitlist!
|
||||
</p>
|
||||
<p class="text-sm text-amber-400 mt-1">
|
||||
Position #{{ waitlistPosition }} - We'll email you if a spot opens up
|
||||
</p>
|
||||
</div>
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="handleLeaveWaitlist"
|
||||
:loading="isJoiningWaitlist"
|
||||
>
|
||||
Leave Waitlist
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- Join Waitlist Form -->
|
||||
<div v-else>
|
||||
<div class="p-4 bg-amber-900/20 rounded-lg border border-amber-800 mb-4">
|
||||
<p class="font-semibold text-amber-300">
|
||||
This event is full
|
||||
</p>
|
||||
<p class="text-sm text-amber-400 mt-1">
|
||||
Join the waitlist and we'll notify you if a spot opens up
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleJoinWaitlist" class="space-y-4">
|
||||
<div v-if="!memberData">
|
||||
<UInput
|
||||
v-model="waitlistForm.email"
|
||||
type="email"
|
||||
placeholder="Your email address"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<UButton
|
||||
type="submit"
|
||||
color="warning"
|
||||
block
|
||||
:loading="isJoiningWaitlist"
|
||||
>
|
||||
{{ isJoiningWaitlist ? "Joining..." : "Join Waitlist" }}
|
||||
</UButton>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -446,16 +573,21 @@ if (error.value?.statusCode === 404) {
|
|||
|
||||
// Authentication
|
||||
const { isMember, memberData, checkMemberStatus } = useAuth();
|
||||
const {
|
||||
isPendingPayment,
|
||||
isSuspended,
|
||||
isCancelled,
|
||||
canRSVP,
|
||||
statusConfig,
|
||||
getRSVPMessage,
|
||||
} = useMemberStatus();
|
||||
const { completePayment, isProcessingPayment, paymentError } =
|
||||
useMemberPayment();
|
||||
|
||||
// Check member status on mount
|
||||
onMounted(async () => {
|
||||
await checkMemberStatus();
|
||||
|
||||
// Debug: Log series data
|
||||
if (event.value?.series) {
|
||||
console.log("Series data:", event.value.series);
|
||||
}
|
||||
|
||||
// Pre-fill form if member is logged in
|
||||
if (memberData.value) {
|
||||
registrationForm.value.name = memberData.value.name;
|
||||
|
|
@ -465,6 +597,9 @@ onMounted(async () => {
|
|||
|
||||
// Check if user is already registered
|
||||
await checkRegistrationStatus();
|
||||
|
||||
// Check waitlist status
|
||||
checkWaitlistStatus();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -507,6 +642,105 @@ const isRegistering = ref(false);
|
|||
const isCancelling = ref(false);
|
||||
const registrationStatus = ref("not-registered"); // 'not-registered', 'registered'
|
||||
|
||||
// Waitlist state
|
||||
const isJoiningWaitlist = ref(false);
|
||||
const isOnWaitlist = ref(false);
|
||||
const waitlistPosition = ref(0);
|
||||
const waitlistForm = ref({
|
||||
email: "",
|
||||
});
|
||||
|
||||
// Computed: Check if event is full
|
||||
const isEventFull = computed(() => {
|
||||
if (!event.value?.maxAttendees) return false;
|
||||
return (event.value.registeredCount || 0) >= event.value.maxAttendees;
|
||||
});
|
||||
|
||||
// Check waitlist status
|
||||
const checkWaitlistStatus = async () => {
|
||||
const email = memberData.value?.email || waitlistForm.value.email;
|
||||
if (!email || !event.value?.tickets?.waitlist?.enabled) return;
|
||||
|
||||
const entries = event.value.tickets.waitlist.entries || [];
|
||||
const entryIndex = entries.findIndex(
|
||||
(e) => e.email.toLowerCase() === email.toLowerCase()
|
||||
);
|
||||
|
||||
if (entryIndex !== -1) {
|
||||
isOnWaitlist.value = true;
|
||||
waitlistPosition.value = entryIndex + 1;
|
||||
} else {
|
||||
isOnWaitlist.value = false;
|
||||
waitlistPosition.value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Join waitlist handler
|
||||
const handleJoinWaitlist = async () => {
|
||||
isJoiningWaitlist.value = true;
|
||||
|
||||
try {
|
||||
const email = memberData.value?.email || waitlistForm.value.email;
|
||||
const name = memberData.value?.name || "Guest";
|
||||
|
||||
const response = await $fetch(`/api/events/${route.params.id}/waitlist`, {
|
||||
method: "POST",
|
||||
body: { email, name },
|
||||
});
|
||||
|
||||
isOnWaitlist.value = true;
|
||||
waitlistPosition.value = response.position;
|
||||
|
||||
toast.add({
|
||||
title: "Added to Waitlist",
|
||||
description: `You're #${response.position} on the waitlist. We'll email you if a spot opens up.`,
|
||||
color: "orange",
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error.data?.statusMessage || "Failed to join waitlist. Please try again.";
|
||||
|
||||
toast.add({
|
||||
title: "Couldn't Join Waitlist",
|
||||
description: errorMessage,
|
||||
color: "red",
|
||||
});
|
||||
} finally {
|
||||
isJoiningWaitlist.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Leave waitlist handler
|
||||
const handleLeaveWaitlist = async () => {
|
||||
isJoiningWaitlist.value = true;
|
||||
|
||||
try {
|
||||
const email = memberData.value?.email || waitlistForm.value.email;
|
||||
|
||||
await $fetch(`/api/events/${route.params.id}/waitlist`, {
|
||||
method: "DELETE",
|
||||
body: { email },
|
||||
});
|
||||
|
||||
isOnWaitlist.value = false;
|
||||
waitlistPosition.value = 0;
|
||||
|
||||
toast.add({
|
||||
title: "Removed from Waitlist",
|
||||
description: "You've been removed from the waitlist.",
|
||||
color: "blue",
|
||||
});
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
title: "Error",
|
||||
description: "Failed to leave waitlist. Please try again.",
|
||||
color: "red",
|
||||
});
|
||||
} finally {
|
||||
isJoiningWaitlist.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Format date for display
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
|
|
@ -671,7 +905,6 @@ const handleCancelRegistration = async () => {
|
|||
|
||||
// Handle ticket purchase success
|
||||
const handleTicketSuccess = (response) => {
|
||||
console.log("Ticket purchased successfully:", response);
|
||||
// Update registered count if needed
|
||||
if (event.value.registeredCount !== undefined) {
|
||||
event.value.registeredCount++;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue