ghostguild-org/app/components/EventTicketPurchase.vue

412 lines
11 KiB
Vue

<template>
<div class="event-ticket-purchase">
<!-- Loading State -->
<div v-if="loading" class="text-center py-8">
<div
class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"
></div>
<p class="text-ghost-300">Loading ticket information...</p>
</div>
<!-- Error State -->
<div
v-else-if="error"
class="p-6 bg-red-900/20 rounded-xl border border-red-800"
>
<h3 class="text-lg font-semibold text-red-300 mb-2">
Unable to Load Tickets
</h3>
<p class="text-red-400">{{ error }}</p>
</div>
<!-- Series Pass Required -->
<div
v-else-if="ticketInfo?.requiresSeriesPass"
class="p-6 bg-purple-900/20 rounded-xl border border-purple-800"
>
<h3
class="text-lg font-semibold text-purple-300 mb-2 flex items-center gap-2"
>
<Icon name="heroicons:ticket" class="w-6 h-6" />
Series Pass Required
</h3>
<p class="text-purple-400 mb-4">
This event is part of
<strong>{{ ticketInfo.series?.title }}</strong> and requires a series
pass to attend.
</p>
<p class="text-sm text-ghost-300 mb-6">
Purchase a series pass to get access to all events in this series.
</p>
<UButton
:to="`/series/${ticketInfo.series?.slug || ticketInfo.series?.id}`"
color="primary"
size="lg"
block
>
View Series & Purchase Pass
</UButton>
</div>
<!-- Already Registered -->
<div
v-else-if="ticketInfo?.alreadyRegistered"
class="p-6 bg-green-900/20 rounded-xl border border-green-800"
>
<h3
class="text-lg font-semibold text-green-300 mb-2 flex items-center gap-2"
>
<Icon name="heroicons:check-circle-solid" class="w-6 h-6" />
You're Registered!
</h3>
<p class="text-green-400 mb-4">
<template v-if="ticketInfo.viaSeriesPass">
You have access to this event via your series pass for
<strong>{{ ticketInfo.series?.title }}</strong
>.
</template>
<template v-else>
You're all set for this event. Check your email for confirmation
details.
</template>
</p>
<p class="text-sm text-ghost-300">
See you on {{ formatEventDate(eventStartDate) }}!
</p>
</div>
<!-- Ticket Selection -->
<div v-else-if="ticketInfo">
<!-- Ticket Card -->
<EventTicketCard
:ticket-info="ticketInfo"
:is-selected="true"
:is-available="ticketInfo.available"
:already-registered="ticketInfo.alreadyRegistered"
class="mb-6"
@join-waitlist="handleJoinWaitlist"
/>
<!-- Registration Form -->
<div v-if="ticketInfo.available && !ticketInfo.alreadyRegistered">
<h3 class="text-xl font-bold text-ghost-100 mb-4">
{{ ticketInfo.isFree ? "Register" : "Purchase Ticket" }}
</h3>
<form @submit.prevent="handleSubmit" class="space-y-4">
<!-- Name Field -->
<div>
<label
for="name"
class="block text-sm font-medium text-ghost-200 mb-2"
>
Full Name
</label>
<UInput
id="name"
v-model="form.name"
type="text"
required
placeholder="Enter your full name"
:disabled="processing"
/>
</div>
<!-- Email Field -->
<div>
<label
for="email"
class="block text-sm font-medium text-ghost-200 mb-2"
>
Email Address
</label>
<UInput
id="email"
v-model="form.email"
type="email"
required
placeholder="Enter your email"
:disabled="processing || isLoggedIn"
/>
<p v-if="isLoggedIn" class="text-xs text-ghost-400 mt-1">
Using your member email
</p>
</div>
<!-- Member Benefits Notice -->
<div
v-if="ticketInfo.isMember && ticketInfo.isFree"
class="p-4 bg-purple-900/20 rounded-lg border border-purple-800"
>
<p class="text-sm text-purple-300 flex items-center gap-2">
<Icon name="heroicons:sparkles" class="w-4 h-4" />
This event is free for Ghost Guild members
</p>
</div>
<!-- Payment Required Notice -->
<div
v-if="!ticketInfo.isFree"
class="p-4 bg-blue-900/20 rounded-lg border border-blue-800"
>
<p class="text-sm text-blue-300 flex items-center gap-2">
<Icon name="heroicons:credit-card" class="w-4 h-4" />
Payment of {{ ticketInfo.formattedPrice }} will be processed
securely
</p>
</div>
<!-- Submit Button -->
<div class="pt-4">
<UButton
type="submit"
color="primary"
size="lg"
block
:loading="processing"
:disabled="!form.name || !form.email"
>
{{
processing
? "Processing..."
: ticketInfo.isFree
? "Complete Registration"
: `Pay ${ticketInfo.formattedPrice}`
}}
</UButton>
</div>
</form>
</div>
<!-- Sold Out with Waitlist -->
<div
v-else-if="!ticketInfo.available && ticketInfo.waitlistAvailable"
class="text-center py-8"
>
<Icon
name="heroicons:ticket"
class="w-16 h-16 text-ghost-400 mx-auto mb-4"
/>
<h3 class="text-xl font-bold text-ghost-100 mb-2">Event Sold Out</h3>
<p class="text-ghost-300 mb-6">
This event is currently at capacity. Join the waitlist to be notified
if spots become available.
</p>
<UButton color="gray" size="lg" @click="handleJoinWaitlist">
Join Waitlist
</UButton>
</div>
<!-- Sold Out (No Waitlist) -->
<div v-else-if="!ticketInfo.available" class="text-center py-8">
<Icon
name="heroicons:x-circle"
class="w-16 h-16 text-red-400 mx-auto mb-4"
/>
<h3 class="text-xl font-bold text-ghost-100 mb-2">Event Sold Out</h3>
<p class="text-ghost-300">
Unfortunately, this event is at capacity and no longer accepting
registrations.
</p>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
eventId: {
type: String,
required: true,
},
eventStartDate: {
type: Date,
required: true,
},
eventTitle: {
type: String,
required: true,
},
userEmail: {
type: String,
default: null,
},
});
const emit = defineEmits(["success", "error"]);
const toast = useToast();
const { initializeTicketPayment, verifyPayment, cleanup } = useHelcimPay();
// State
const loading = ref(true);
const processing = ref(false);
const error = ref(null);
const ticketInfo = ref(null);
const form = ref({
name: "",
email: props.userEmail || "",
});
const isLoggedIn = computed(() => !!props.userEmail);
// Fetch ticket availability on mount
onMounted(async () => {
await fetchTicketInfo();
});
const fetchTicketInfo = async () => {
loading.value = true;
error.value = null;
try {
// First check if this event requires a series pass
if (props.userEmail) {
try {
const seriesAccess = await $fetch(
`/api/events/${props.eventId}/check-series-access`,
);
if (seriesAccess.requiresSeriesPass) {
if (seriesAccess.hasSeriesPass) {
// User has series pass - show as already registered
ticketInfo.value = {
available: true,
alreadyRegistered: true,
viaSeriesPass: true,
series: seriesAccess.series,
message: seriesAccess.message,
};
loading.value = false;
return;
} else {
// User needs to buy series pass
ticketInfo.value = {
available: false,
requiresSeriesPass: true,
series: seriesAccess.series,
message: seriesAccess.message,
};
loading.value = false;
return;
}
}
} catch (seriesErr) {
// If series check fails, continue with regular ticket check
console.warn("Series access check failed:", seriesErr);
}
}
// Regular ticket availability check
const params = props.userEmail ? `?email=${props.userEmail}` : "";
const response = await $fetch(
`/api/events/${props.eventId}/tickets/available${params}`,
);
ticketInfo.value = response;
} catch (err) {
console.error("Error fetching ticket info:", err);
error.value =
err.data?.statusMessage || "Failed to load ticket information";
} finally {
loading.value = false;
}
};
const handleSubmit = async () => {
processing.value = true;
try {
let transactionId = null;
// If payment is required, initialize Helcim and process payment
if (!ticketInfo.value.isFree) {
// Initialize Helcim payment
await initializeTicketPayment(
props.eventId,
form.value.email,
ticketInfo.value.price,
props.eventTitle,
);
// Show Helcim modal and complete payment
const paymentResult = await verifyPayment();
if (!paymentResult.success) {
throw new Error("Payment was not completed");
}
// For purchase transactions, we get a transactionId
transactionId = paymentResult.transactionId;
if (!transactionId) {
throw new Error("No transaction ID received from payment");
}
}
// Purchase ticket
const response = await $fetch(
`/api/events/${props.eventId}/tickets/purchase`,
{
method: "POST",
body: {
name: form.value.name,
email: form.value.email,
transactionId,
},
},
);
// Success!
toast.add({
title: "Success!",
description: ticketInfo.value.isFree
? "You're registered for this event"
: "Ticket purchased successfully!",
color: "green",
});
emit("success", response);
// Refresh ticket info to show registered state
await fetchTicketInfo();
} catch (err) {
console.error("Error purchasing ticket:", err);
const errorMessage =
err.data?.statusMessage ||
err.message ||
"Failed to process registration";
toast.add({
title: "Registration Failed",
description: errorMessage,
color: "red",
});
emit("error", err);
} finally {
processing.value = false;
cleanup();
}
};
const handleJoinWaitlist = () => {
// TODO: Implement waitlist functionality
toast.add({
title: "Waitlist",
description: "Waitlist functionality coming soon!",
color: "blue",
});
};
const formatEventDate = (date) => {
return new Date(date).toLocaleDateString("en-US", {
weekday: "long",
month: "long",
day: "numeric",
year: "numeric",
});
};
</script>