412 lines
11 KiB
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>
|