264 lines
8.7 KiB
Vue
264 lines
8.7 KiB
Vue
<template>
|
|
<div
|
|
class="series-ticket-card border border-ghost-600 dark:border-ghost-600 rounded-xl overflow-hidden"
|
|
>
|
|
<!-- Header -->
|
|
<div
|
|
class="bg-gradient-to-br from-purple-600 to-purple-800 dark:from-purple-700 dark:to-purple-900 p-6"
|
|
>
|
|
<div class="flex items-start justify-between gap-4">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<Icon
|
|
name="heroicons:ticket"
|
|
class="w-5 h-5 text-purple-200 dark:text-purple-300"
|
|
/>
|
|
<span class="text-sm font-semibold text-purple-200 dark:text-purple-300">
|
|
Series Pass
|
|
</span>
|
|
</div>
|
|
<h3 class="text-xl font-bold text-white mb-1">
|
|
{{ ticket.name }}
|
|
</h3>
|
|
<p v-if="ticket.description" class="text-sm text-purple-200 dark:text-purple-300">
|
|
{{ ticket.description }}
|
|
</p>
|
|
</div>
|
|
<div class="text-right flex-shrink-0">
|
|
<div class="text-3xl font-bold text-white">
|
|
{{ formatPrice(ticket.price, ticket.currency) }}
|
|
</div>
|
|
<div
|
|
v-if="ticket.isEarlyBird"
|
|
class="text-xs text-purple-200 dark:text-purple-300 mt-1"
|
|
>
|
|
Early Bird Price
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
<div class="p-6 bg-ghost-800/50 dark:bg-ghost-700/30">
|
|
<!-- What's Included -->
|
|
<div class="mb-6">
|
|
<h4 class="text-sm font-semibold text-ghost-200 dark:text-ghost-200 mb-3 uppercase tracking-wide">
|
|
What's Included
|
|
</h4>
|
|
<div class="space-y-2">
|
|
<div class="flex items-center gap-2 text-ghost-300 dark:text-ghost-300">
|
|
<Icon name="heroicons:check-circle" class="w-5 h-5 text-green-400" />
|
|
<span>Access to all {{ totalEvents }} events in the series</span>
|
|
</div>
|
|
<div
|
|
v-if="ticket.isFree && !isMember"
|
|
class="flex items-center gap-2 text-ghost-300 dark:text-ghost-300"
|
|
>
|
|
<Icon name="heroicons:check-circle" class="w-5 h-5 text-green-400" />
|
|
<span>Automatic registration for all sessions</span>
|
|
</div>
|
|
<div
|
|
v-if="memberSavings > 0"
|
|
class="flex items-center gap-2 text-ghost-300 dark:text-ghost-300"
|
|
>
|
|
<Icon name="heroicons:check-circle" class="w-5 h-5 text-green-400" />
|
|
<span>Save {{ formatPrice(memberSavings, ticket.currency) }} as a member</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Events List Preview -->
|
|
<div v-if="events && events.length > 0" class="mb-6">
|
|
<h4 class="text-sm font-semibold text-ghost-200 dark:text-ghost-200 mb-3 uppercase tracking-wide">
|
|
Series Schedule
|
|
</h4>
|
|
<div class="space-y-2">
|
|
<div
|
|
v-for="(event, index) in events.slice(0, 3)"
|
|
:key="event.id"
|
|
class="flex items-start gap-3 p-3 bg-ghost-700/50 dark:bg-ghost-600/30 rounded-lg"
|
|
>
|
|
<div
|
|
class="w-8 h-8 rounded-full bg-purple-600/20 border border-purple-500/30 flex items-center justify-center flex-shrink-0"
|
|
>
|
|
<span class="text-sm font-bold text-purple-300">{{ index + 1 }}</span>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="font-medium text-ghost-100 dark:text-ghost-100 text-sm">
|
|
{{ event.title }}
|
|
</div>
|
|
<div class="text-xs text-ghost-400 dark:text-ghost-400 mt-1">
|
|
{{ formatEventDate(event.startDate) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="events.length > 3"
|
|
class="text-center text-sm text-ghost-400 dark:text-ghost-400 pt-2"
|
|
>
|
|
+ {{ events.length - 3 }} more event{{ events.length - 3 !== 1 ? 's' : '' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Member Benefit Callout -->
|
|
<div
|
|
v-if="ticket.isFree && isMember"
|
|
class="p-4 bg-green-900/20 border border-green-700/30 rounded-lg mb-6"
|
|
>
|
|
<div class="flex items-start gap-3">
|
|
<Icon name="heroicons:sparkles" class="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<div class="font-semibold text-green-300 mb-1">Member Benefit</div>
|
|
<div class="text-sm text-green-400">
|
|
This series pass is free for Ghost Guild members! Complete registration to secure your spot.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Public vs Member Pricing -->
|
|
<div
|
|
v-if="!ticket.isFree && publicPrice && ticket.type === 'member'"
|
|
class="p-4 bg-blue-900/20 border border-blue-700/30 rounded-lg mb-6"
|
|
>
|
|
<div class="flex items-start gap-3">
|
|
<Icon name="heroicons:tag" class="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" />
|
|
<div class="flex-1">
|
|
<div class="font-semibold text-blue-300 mb-1">Member Savings</div>
|
|
<div class="text-sm text-blue-400">
|
|
You're saving {{ formatPrice(memberSavings, ticket.currency) }} as a member.
|
|
Public price: {{ formatPrice(publicPrice, ticket.currency) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Availability -->
|
|
<div v-if="availability" class="mb-6">
|
|
<div
|
|
v-if="!availability.unlimited && availability.remaining !== null"
|
|
class="flex items-center gap-2"
|
|
>
|
|
<Icon
|
|
:name="availability.remaining > 5 ? 'heroicons:check-circle' : 'heroicons:exclamation-triangle'"
|
|
:class="[
|
|
'w-5 h-5',
|
|
availability.remaining > 5 ? 'text-green-400' : 'text-yellow-400'
|
|
]"
|
|
/>
|
|
<span
|
|
:class="[
|
|
'text-sm font-medium',
|
|
availability.remaining > 5 ? 'text-green-300' : 'text-yellow-300'
|
|
]"
|
|
>
|
|
{{ availability.remaining }} series pass{{ availability.remaining !== 1 ? 'es' : '' }} remaining
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sold Out / Waitlist -->
|
|
<div v-if="!available" class="space-y-3">
|
|
<div class="p-4 bg-red-900/20 border border-red-700/30 rounded-lg">
|
|
<div class="flex items-start gap-3">
|
|
<Icon name="heroicons:exclamation-circle" class="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<div class="font-semibold text-red-300 mb-1">Series Pass Sold Out</div>
|
|
<div class="text-sm text-red-400">
|
|
All series passes have been claimed.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<UButton
|
|
v-if="availability?.waitlistAvailable"
|
|
block
|
|
color="gray"
|
|
size="lg"
|
|
@click="$emit('join-waitlist')"
|
|
>
|
|
Join Waitlist
|
|
</UButton>
|
|
</div>
|
|
|
|
<!-- Already Registered -->
|
|
<div v-else-if="alreadyRegistered" class="p-4 bg-green-900/20 border border-green-700/30 rounded-lg">
|
|
<div class="flex items-start gap-3">
|
|
<Icon name="heroicons:check-badge" class="w-6 h-6 text-green-400 flex-shrink-0" />
|
|
<div>
|
|
<div class="font-semibold text-green-300 mb-1">You're Registered!</div>
|
|
<div class="text-sm text-green-400">
|
|
You have a series pass and are registered for all {{ totalEvents }} events.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
const props = defineProps({
|
|
ticket: {
|
|
type: Object,
|
|
required: true,
|
|
// Expected: { name, description, price, currency, type, isFree, isEarlyBird }
|
|
},
|
|
availability: {
|
|
type: Object,
|
|
default: null,
|
|
// Expected: { remaining, unlimited, waitlistAvailable }
|
|
},
|
|
available: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
alreadyRegistered: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
isMember: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
totalEvents: {
|
|
type: Number,
|
|
required: true,
|
|
},
|
|
events: {
|
|
type: Array,
|
|
default: () => [],
|
|
// Expected: Array of { id, title, startDate }
|
|
},
|
|
publicPrice: {
|
|
type: Number,
|
|
default: null,
|
|
},
|
|
});
|
|
|
|
defineEmits(['join-waitlist']);
|
|
|
|
const memberSavings = computed(() => {
|
|
if (props.publicPrice && props.ticket.price < props.publicPrice) {
|
|
return props.publicPrice - props.ticket.price;
|
|
}
|
|
return 0;
|
|
});
|
|
|
|
const formatPrice = (price, currency = "CAD") => {
|
|
if (price === 0) return "Free";
|
|
return new Intl.NumberFormat("en-CA", {
|
|
style: "currency",
|
|
currency,
|
|
}).format(price);
|
|
};
|
|
|
|
const formatEventDate = (date) => {
|
|
return new Date(date).toLocaleDateString("en-US", {
|
|
month: "short",
|
|
day: "numeric",
|
|
year: "numeric",
|
|
});
|
|
};
|
|
</script>
|