Migrates event/series UI from Tailwind/Nuxt UI form components to the zine pattern (dashed borders, CSS-var palette, native inputs). Restructures single-event and series detail/index pages to the two-column grid pattern matching about.vue and member/dashboard.vue. Touches: - app/components/EventSeriesTicketCard.vue — phantom-palette → CSS-var migration (--candle, --ember, --surface), color="gray" → "neutral" - app/components/EventTicketCard.vue — --candle-faint border var - app/components/EventTicketPurchase.vue — accent-color: var(--candle) - app/pages/events/[slug].vue — page-fill flex chain, .event-body grid - app/pages/events/index.vue — series link uses series.id (was _id) - app/pages/series/[id].vue — two-column layout (1fr/280px) + sidebar - app/pages/series/index.vue — full rewrite to dashed-border zine pattern Per docs/specs/events-visual-audit-findings.md Phase 4. Behavior unchanged.
236 lines
5.1 KiB
Vue
236 lines
5.1 KiB
Vue
<template>
|
|
<div
|
|
class="ticket-card"
|
|
:class="{
|
|
'is-selected': isSelected,
|
|
'is-unavailable': !isAvailable || alreadyRegistered,
|
|
}"
|
|
@click="handleClick"
|
|
>
|
|
<!-- Ticket Header -->
|
|
<div class="ticket-header">
|
|
<div>
|
|
<h3 class="ticket-name">{{ ticketInfo.name }}</h3>
|
|
<p v-if="ticketInfo.description" class="ticket-desc">
|
|
{{ ticketInfo.description }}
|
|
</p>
|
|
</div>
|
|
<span v-if="ticketInfo.isMember" class="badge">Members Only</span>
|
|
</div>
|
|
|
|
<!-- Price Display -->
|
|
<div class="ticket-price-block">
|
|
<div class="ticket-price-row">
|
|
<span
|
|
class="ticket-price"
|
|
:class="{ 'is-free': ticketInfo.isFree }"
|
|
>
|
|
{{ ticketInfo.formattedPrice }}
|
|
</span>
|
|
<span v-if="ticketInfo.isEarlyBird" class="badge early-bird">
|
|
Early Bird
|
|
</span>
|
|
</div>
|
|
|
|
<div v-if="ticketInfo.isEarlyBird && ticketInfo.formattedRegularPrice" class="ticket-regular-price">
|
|
Regular: {{ ticketInfo.formattedRegularPrice }}
|
|
</div>
|
|
|
|
<div v-if="ticketInfo.isEarlyBird && ticketInfo.earlyBirdDeadline" class="ticket-deadline">
|
|
Early bird ends {{ formatDeadline(ticketInfo.earlyBirdDeadline) }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Member Savings -->
|
|
<div
|
|
v-if="ticketInfo.publicTicket && ticketInfo.memberSavings > 0"
|
|
class="ticket-savings"
|
|
>
|
|
<p>You save {{ formatPrice(ticketInfo.memberSavings) }} as a member!</p>
|
|
<p class="ticket-savings-detail">
|
|
Public price: {{ ticketInfo.publicTicket.formattedPrice }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Availability -->
|
|
<div class="ticket-availability">
|
|
<span v-if="alreadyRegistered" class="status-registered">
|
|
You're registered
|
|
</span>
|
|
<span v-else-if="!isAvailable" class="status-sold-out">
|
|
Sold Out
|
|
</span>
|
|
<span v-else-if="ticketInfo.remaining !== null" class="status-remaining">
|
|
{{ ticketInfo.remaining }} remaining
|
|
</span>
|
|
<span v-else class="status-remaining">
|
|
Unlimited availability
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Waitlist Option -->
|
|
<div
|
|
v-if="!isAvailable && ticketInfo.waitlistAvailable && !alreadyRegistered"
|
|
class="ticket-waitlist"
|
|
>
|
|
<button class="btn" @click.stop="$emit('join-waitlist')">
|
|
Join Waitlist
|
|
</button>
|
|
</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 (diff < 24 * 60 * 60 * 1000) {
|
|
const hours = Math.floor(diff / (60 * 60 * 1000));
|
|
return `in ${hours} hour${hours !== 1 ? "s" : ""}`;
|
|
}
|
|
|
|
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 {
|
|
border-bottom: 1px dashed var(--border);
|
|
padding: 20px 24px;
|
|
transition: border-color 0.15s;
|
|
cursor: default;
|
|
}
|
|
.ticket-card.is-selected {
|
|
border-color: var(--candle-faint);
|
|
}
|
|
.ticket-card.is-unavailable {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
.ticket-card:not(.is-unavailable) {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.ticket-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.ticket-name {
|
|
font-family: "Brygada 1918", serif;
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
color: var(--text-bright);
|
|
}
|
|
.ticket-desc {
|
|
font-size: 11px;
|
|
color: var(--text-dim);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.ticket-price-block {
|
|
margin-bottom: 10px;
|
|
}
|
|
.ticket-price-row {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 8px;
|
|
}
|
|
.ticket-price {
|
|
font-size: 22px;
|
|
font-weight: 600;
|
|
color: var(--text-bright);
|
|
}
|
|
.ticket-price.is-free {
|
|
color: var(--candle);
|
|
}
|
|
.ticket-regular-price {
|
|
font-size: 11px;
|
|
color: var(--text-faint);
|
|
text-decoration: line-through;
|
|
margin-top: 2px;
|
|
}
|
|
.ticket-deadline {
|
|
font-size: 10px;
|
|
color: var(--candle-dim);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.early-bird {
|
|
color: var(--candle-dim);
|
|
border-color: var(--candle-faint);
|
|
}
|
|
|
|
.ticket-savings {
|
|
border: 1px dashed var(--candle-faint);
|
|
padding: 8px 12px;
|
|
margin-bottom: 10px;
|
|
font-size: 11px;
|
|
color: var(--candle);
|
|
}
|
|
.ticket-savings-detail {
|
|
font-size: 10px;
|
|
color: var(--text-faint);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.ticket-availability {
|
|
font-size: 11px;
|
|
}
|
|
.status-registered {
|
|
color: var(--green);
|
|
}
|
|
.status-sold-out {
|
|
color: var(--ember);
|
|
}
|
|
.status-remaining {
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.ticket-waitlist {
|
|
margin-top: 12px;
|
|
padding-top: 12px;
|
|
border-top: 1px dashed var(--border);
|
|
}
|
|
</style>
|