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