414 lines
9.8 KiB
Vue
414 lines
9.8 KiB
Vue
<template>
|
||
<div v-if="pending" class="loading">Loading event details...</div>
|
||
|
||
<div v-else-if="error" class="loading">
|
||
<h2>Event Not Found</h2>
|
||
<p>The event you're looking for doesn't exist.</p>
|
||
<NuxtLink to="/events">← Back to Events</NuxtLink>
|
||
</div>
|
||
|
||
<div v-else>
|
||
<!-- EVENT HEADER -->
|
||
<div class="event-header">
|
||
<h1>{{ event.title }}</h1>
|
||
<div class="event-meta-row">
|
||
<div class="event-meta-item">
|
||
<span class="meta-label">Date</span>
|
||
{{ formatDate(event.startDate) }}
|
||
</div>
|
||
<div class="event-meta-item">
|
||
<span class="meta-label">Time</span>
|
||
{{ formatTime(event.startDate, event.endDate) }}
|
||
</div>
|
||
<div class="event-meta-item">
|
||
<span class="meta-label">Location</span>
|
||
{{ event.location }}
|
||
</div>
|
||
<div v-if="event.circle" class="event-meta-item">
|
||
<CircleBadge :circle="event.circle" />
|
||
</div>
|
||
<div v-if="event.maxAttendees" class="event-meta-item">
|
||
<span class="meta-label">Capacity</span>
|
||
{{ event.registeredCount || 0 }}/{{ event.maxAttendees }} seats
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CANCELLED NOTICE -->
|
||
<div v-if="event.isCancelled" class="cancelled-notice">
|
||
<strong>Event Cancelled</strong>
|
||
<p v-if="event.cancellationMessage">{{ event.cancellationMessage }}</p>
|
||
<p v-else>
|
||
This event has been cancelled. We apologize for any inconvenience.
|
||
</p>
|
||
</div>
|
||
|
||
<!-- FEATURE IMAGE -->
|
||
<div v-if="event.featureImage?.url" class="event-feature-image">
|
||
<img
|
||
:src="event.featureImage.url"
|
||
:alt="event.featureImage.alt || event.title"
|
||
/>
|
||
</div>
|
||
|
||
<!-- TWO-COLUMN BODY -->
|
||
<div class="event-body">
|
||
<!-- LEFT: MAIN CONTENT -->
|
||
<div class="event-main">
|
||
<!-- Series Badge -->
|
||
<div v-if="event.series?.isSeriesEvent" class="section">
|
||
<div class="series-note">
|
||
<span class="section-label">Part of Series</span>
|
||
<NuxtLink :to="`/series/${event.series.id}`">{{
|
||
event.series.title
|
||
}}</NuxtLink>
|
||
— Event {{ event.series.position }} of
|
||
{{ event.series.totalEvents }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Target Circles -->
|
||
<div v-if="event.targetCircles?.length" class="section">
|
||
<span class="section-label">Recommended for</span>
|
||
<div class="circle-badges">
|
||
<CircleBadge
|
||
v-for="circle in event.targetCircles"
|
||
:key="circle"
|
||
:circle="circle"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Description -->
|
||
<div class="section">
|
||
<h2>About This Event</h2>
|
||
<p>{{ event.description }}</p>
|
||
</div>
|
||
|
||
<!-- Series Description -->
|
||
<div
|
||
v-if="event.series?.isSeriesEvent && event.series.description"
|
||
class="section"
|
||
>
|
||
<h2>About the {{ event.series.title }} Series</h2>
|
||
<p>{{ event.series.description }}</p>
|
||
</div>
|
||
|
||
<!-- Agenda -->
|
||
<div v-if="event.agenda?.length" class="section">
|
||
<h2>Agenda</h2>
|
||
<ol class="agenda-list">
|
||
<li v-for="(item, index) in event.agenda" :key="index">
|
||
{{ item }}
|
||
</li>
|
||
</ol>
|
||
</div>
|
||
|
||
<!-- Speakers -->
|
||
<div v-if="event.speakers?.length" class="section">
|
||
<h2>Speakers</h2>
|
||
<div
|
||
v-for="speaker in event.speakers"
|
||
:key="speaker.name"
|
||
class="speaker"
|
||
>
|
||
<div class="speaker-name">{{ speaker.name }}</div>
|
||
<div v-if="speaker.role" class="speaker-role">
|
||
{{ speaker.role }}
|
||
</div>
|
||
<div v-if="speaker.bio" class="speaker-bio">{{ speaker.bio }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RIGHT: SIDEBAR PANELS -->
|
||
<div v-if="!event.isCancelled" class="event-aside">
|
||
<!-- Ticket System -->
|
||
<EventTicketPurchase
|
||
:event-id="event._id || event.id"
|
||
:event-start-date="event.startDate"
|
||
:event-title="event.title"
|
||
:user-email="memberData?.email"
|
||
:user-name="memberData?.name"
|
||
@success="handleTicketSuccess"
|
||
@error="handleTicketError"
|
||
/>
|
||
|
||
<!-- Event Details Box -->
|
||
<div class="dashed-box">
|
||
<div class="box-title">Event Details</div>
|
||
<div v-if="event.eventType" class="detail-row">
|
||
<span class="detail-key">Type</span>
|
||
<span class="detail-val">{{ event.eventType }}</span>
|
||
</div>
|
||
<div v-if="event.membersOnly" class="detail-row">
|
||
<span class="detail-key">Members only</span>
|
||
<span class="detail-val">Yes</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Questions -->
|
||
<div class="dashed-box">
|
||
<div class="box-title">Questions?</div>
|
||
<p
|
||
style="font-size: 12px; color: var(--text-dim); margin-bottom: 4px"
|
||
>
|
||
Drop us a line.
|
||
</p>
|
||
<a href="mailto:events@ghostguild.org" style="font-size: 12px"
|
||
>events@ghostguild.org</a
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
const route = useRoute();
|
||
const toast = useToast();
|
||
|
||
const {
|
||
data: event,
|
||
pending,
|
||
error,
|
||
} = await useFetch(`/api/events/${route.params.slug}`);
|
||
|
||
const pageBreadcrumbTitle = useState("pageBreadcrumbTitle", () => "");
|
||
pageBreadcrumbTitle.value = event.value?.title || "";
|
||
onUnmounted(() => {
|
||
pageBreadcrumbTitle.value = "";
|
||
});
|
||
|
||
if (error.value?.statusCode === 404) {
|
||
throw createError({ statusCode: 404, statusMessage: "Event not found" });
|
||
}
|
||
|
||
const { memberData, checkMemberStatus } = useAuth();
|
||
const { trackGoal, isComplete } = useOnboarding();
|
||
|
||
onMounted(async () => {
|
||
await checkMemberStatus();
|
||
if (memberData.value && !isComplete.value) {
|
||
trackGoal('eventPageVisited');
|
||
}
|
||
});
|
||
|
||
const formatDate = (dateStr) => {
|
||
const d = new Date(dateStr);
|
||
return new Intl.DateTimeFormat("en-US", {
|
||
weekday: "long",
|
||
year: "numeric",
|
||
month: "long",
|
||
day: "numeric",
|
||
}).format(d);
|
||
};
|
||
|
||
const formatTime = (start, end) => {
|
||
const fmt = new Intl.DateTimeFormat("en-US", {
|
||
hour: "numeric",
|
||
minute: "2-digit",
|
||
timeZoneName: "short",
|
||
});
|
||
return `${fmt.format(new Date(start))} – ${fmt.format(new Date(end))}`;
|
||
};
|
||
|
||
const handleTicketSuccess = () => {
|
||
if (event.value.registeredCount !== undefined) event.value.registeredCount++;
|
||
};
|
||
const handleTicketError = (err) => {
|
||
console.error("Ticket purchase failed:", err);
|
||
};
|
||
|
||
useHead(() => ({
|
||
title: event.value
|
||
? `${event.value.title} - Ghost Guild Events`
|
||
: "Event - Ghost Guild",
|
||
meta: [
|
||
{
|
||
name: "description",
|
||
content: event.value?.description || "View event details and register",
|
||
},
|
||
],
|
||
}));
|
||
</script>
|
||
|
||
<style scoped>
|
||
.loading {
|
||
padding: 48px 32px;
|
||
color: var(--text-dim);
|
||
}
|
||
.loading h2 {
|
||
font-family: "Brygada 1918", serif;
|
||
font-size: 22px;
|
||
color: var(--text-bright);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.event-feature-image {
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
.event-feature-image img {
|
||
display: block;
|
||
width: 100%;
|
||
max-height: 400px;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.event-header {
|
||
padding: 28px 32px;
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
.event-header h1 {
|
||
font-family: "Brygada 1918", serif;
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
color: var(--text-bright);
|
||
line-height: 1.15;
|
||
margin-bottom: 16px;
|
||
}
|
||
.event-meta-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 24px;
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
}
|
||
.meta-label {
|
||
font-size: 10px;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
color: var(--text-faint);
|
||
display: block;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.cancelled-notice {
|
||
padding: 20px 32px;
|
||
border-bottom: 1px dashed var(--border);
|
||
color: var(--ember);
|
||
font-size: 12px;
|
||
}
|
||
.cancelled-notice strong {
|
||
display: block;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
/* ---- TWO-COLUMN BODY ---- */
|
||
.event-body {
|
||
display: grid;
|
||
grid-template-columns: 1fr 280px;
|
||
}
|
||
.event-main {
|
||
min-width: 0;
|
||
}
|
||
.event-aside {
|
||
border-left: 1px dashed var(--border);
|
||
padding: 0;
|
||
}
|
||
.event-aside .dashed-box {
|
||
margin: 0;
|
||
border: none;
|
||
border-bottom: 1px dashed var(--border);
|
||
padding: 20px 24px;
|
||
}
|
||
.event-aside .dashed-box:hover {
|
||
border-color: var(--border);
|
||
}
|
||
|
||
.section {
|
||
padding: 24px 32px;
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
.section h2 {
|
||
font-family: "Brygada 1918", serif;
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
color: var(--text-bright);
|
||
margin-bottom: 8px;
|
||
}
|
||
.section p {
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
line-height: 1.7;
|
||
max-width: 560px;
|
||
}
|
||
|
||
.circle-badges {
|
||
display: flex;
|
||
gap: 6px;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.series-note {
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.agenda-list {
|
||
padding-left: 20px;
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
line-height: 2;
|
||
}
|
||
|
||
.speaker {
|
||
padding: 8px 0;
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
.speaker:last-child {
|
||
border-bottom: none;
|
||
}
|
||
.speaker-name {
|
||
font-size: 13px;
|
||
color: var(--text-bright);
|
||
font-weight: 500;
|
||
}
|
||
.speaker-role {
|
||
font-size: 11px;
|
||
color: var(--text-dim);
|
||
}
|
||
.speaker-bio {
|
||
font-size: 11px;
|
||
color: var(--text-faint);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.box-title {
|
||
font-size: 10px;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
color: var(--text-faint);
|
||
margin-bottom: 8px;
|
||
}
|
||
.detail-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 4px 0;
|
||
font-size: 12px;
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
.detail-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
.detail-key {
|
||
color: var(--text-faint);
|
||
}
|
||
.detail-val {
|
||
color: var(--text);
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.event-body {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.event-aside {
|
||
border-left: none;
|
||
border-top: 1px dashed var(--border);
|
||
}
|
||
.event-meta-row {
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
}
|
||
</style>
|