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

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

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;