192 lines
5.2 KiB
Vue
192 lines
5.2 KiB
Vue
<template>
|
|
<div
|
|
class="ticket-card rounded-xl border p-6 transition-all duration-200"
|
|
:class="[
|
|
isSelected
|
|
? 'border-primary bg-primary/5'
|
|
: 'border-ghost-600 bg-ghost-800/50',
|
|
isAvailable && !alreadyRegistered
|
|
? 'hover:border-primary/50 cursor-pointer'
|
|
: 'opacity-60 cursor-not-allowed',
|
|
]"
|
|
@click="handleClick"
|
|
>
|
|
<!-- Ticket Header -->
|
|
<div class="flex items-start justify-between mb-4">
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-ghost-100">
|
|
{{ ticketInfo.name }}
|
|
</h3>
|
|
<p v-if="ticketInfo.description" class="text-sm text-ghost-300 mt-1">
|
|
{{ ticketInfo.description }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Badge -->
|
|
<div v-if="ticketInfo.isMember" class="flex-shrink-0 ml-4">
|
|
<span
|
|
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400"
|
|
>
|
|
Members Only
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Price Display -->
|
|
<div class="mb-4">
|
|
<div class="flex items-baseline gap-2">
|
|
<span
|
|
class="text-3xl font-bold"
|
|
:class="ticketInfo.isFree ? 'text-green-400' : 'text-ghost-100'"
|
|
>
|
|
{{ ticketInfo.formattedPrice }}
|
|
</span>
|
|
|
|
<!-- Early Bird Badge -->
|
|
<span
|
|
v-if="ticketInfo.isEarlyBird"
|
|
class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"
|
|
>
|
|
Early Bird
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Regular Price (if early bird) -->
|
|
<div
|
|
v-if="ticketInfo.isEarlyBird && ticketInfo.formattedRegularPrice"
|
|
class="mt-1"
|
|
>
|
|
<span class="text-sm text-ghost-400 line-through">
|
|
Regular: {{ ticketInfo.formattedRegularPrice }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Early Bird Countdown -->
|
|
<div
|
|
v-if="ticketInfo.isEarlyBird && ticketInfo.earlyBirdDeadline"
|
|
class="mt-2 text-xs text-amber-400"
|
|
>
|
|
<Icon name="heroicons:clock" class="w-4 h-4 inline mr-1" />
|
|
Early bird ends {{ formatDeadline(ticketInfo.earlyBirdDeadline) }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Member Savings -->
|
|
<div
|
|
v-if="ticketInfo.publicTicket && ticketInfo.memberSavings > 0"
|
|
class="mb-4 p-3 bg-green-900/20 rounded-lg border border-green-800"
|
|
>
|
|
<p class="text-sm text-green-400">
|
|
<Icon name="heroicons:check-circle" class="w-4 h-4 inline mr-1" />
|
|
You save {{ formatPrice(ticketInfo.memberSavings) }} as a member!
|
|
</p>
|
|
<p class="text-xs text-ghost-400 mt-1">
|
|
Public price: {{ ticketInfo.publicTicket.formattedPrice }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Availability -->
|
|
<div class="flex items-center justify-between text-sm">
|
|
<div>
|
|
<span
|
|
v-if="alreadyRegistered"
|
|
class="text-green-400 flex items-center gap-1"
|
|
>
|
|
<Icon name="heroicons:check-circle-solid" class="w-4 h-4" />
|
|
You're registered
|
|
</span>
|
|
<span
|
|
v-else-if="!isAvailable"
|
|
class="text-red-400 flex items-center gap-1"
|
|
>
|
|
<Icon name="heroicons:x-circle-solid" class="w-4 h-4" />
|
|
Sold Out
|
|
</span>
|
|
<span v-else-if="ticketInfo.remaining !== null" class="text-ghost-300">
|
|
{{ ticketInfo.remaining }} remaining
|
|
</span>
|
|
<span v-else class="text-ghost-300"> Unlimited availability </span>
|
|
</div>
|
|
|
|
<!-- Selection Indicator -->
|
|
<div v-if="isSelected && isAvailable && !alreadyRegistered">
|
|
<Icon name="heroicons:check-circle-solid" class="w-5 h-5 text-primary" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Waitlist Option -->
|
|
<div
|
|
v-if="!isAvailable && ticketInfo.waitlistAvailable && !alreadyRegistered"
|
|
class="mt-4 pt-4 border-t border-ghost-600"
|
|
>
|
|
<UButton
|
|
color="gray"
|
|
size="sm"
|
|
block
|
|
@click.stop="$emit('join-waitlist')"
|
|
>
|
|
Join Waitlist
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
const props = defineProps({
|
|
ticketInfo: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
isSelected: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
isAvailable: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
alreadyRegistered: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
});
|
|
|
|
const emit = defineEmits(["select", "join-waitlist"]);
|
|
|
|
const handleClick = () => {
|
|
if (props.isAvailable && !props.alreadyRegistered) {
|
|
emit("select");
|
|
}
|
|
};
|
|
|
|
const formatDeadline = (deadline) => {
|
|
const date = new Date(deadline);
|
|
const now = new Date();
|
|
const diff = date - now;
|
|
|
|
// If less than 24 hours, show hours
|
|
if (diff < 24 * 60 * 60 * 1000) {
|
|
const hours = Math.floor(diff / (60 * 60 * 1000));
|
|
return `in ${hours} hour${hours !== 1 ? "s" : ""}`;
|
|
}
|
|
|
|
// Otherwise show date
|
|
return `on ${date.toLocaleDateString("en-US", {
|
|
month: "short",
|
|
day: "numeric",
|
|
})}`;
|
|
};
|
|
|
|
const formatPrice = (amount) => {
|
|
return new Intl.NumberFormat("en-CA", {
|
|
style: "currency",
|
|
currency: "CAD",
|
|
}).format(amount);
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.ticket-card {
|
|
position: relative;
|
|
}
|
|
</style>
|