feat(events): markdown body, registered indicator, drop capacity counters
- Public event detail page renders description/series-description/content as markdown via useMarkdown, with prose styles; agenda becomes an unordered list with the same custom bullets. - Events list API returns `isRegistered` per event (derived from the requester's registrations, ignoring cancelled rows), and the list page shows a "Registered" tag. Stops exposing the full registrations array in the list response. - Removes the seats/sold-out/limited capacity UI from list and detail pages. - EventTicketPurchase: minor template formatting (self-closing inputs, prettier wrapping).
This commit is contained in:
parent
2ffaf0ef09
commit
622cc8e53b
4 changed files with 176 additions and 118 deletions
|
|
@ -38,14 +38,14 @@
|
||||||
|
|
||||||
<!-- Already Registered -->
|
<!-- Already Registered -->
|
||||||
<div v-else-if="ticketInfo?.alreadyRegistered" class="ticket-panel">
|
<div v-else-if="ticketInfo?.alreadyRegistered" class="ticket-panel">
|
||||||
<div class="box-title">Registration</div>
|
|
||||||
<p class="ticket-status" style="color: var(--green)">
|
<p class="ticket-status" style="color: var(--green)">
|
||||||
You're Registered!
|
You're Registered!
|
||||||
</p>
|
</p>
|
||||||
<p class="ticket-detail">
|
<p class="ticket-detail">
|
||||||
<template v-if="ticketInfo.viaSeriesPass">
|
<template v-if="ticketInfo.viaSeriesPass">
|
||||||
You have access to this event via your series pass for
|
You have access to this event via your series pass for
|
||||||
<strong>{{ ticketInfo.series?.title }}</strong>.
|
<strong>{{ ticketInfo.series?.title }}</strong
|
||||||
|
>.
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
You're all set for this event. Check your email for confirmation
|
You're all set for this event. Check your email for confirmation
|
||||||
|
|
@ -70,13 +70,11 @@
|
||||||
|
|
||||||
<!-- Registration (logged-in member) -->
|
<!-- Registration (logged-in member) -->
|
||||||
<div
|
<div
|
||||||
v-if="ticketInfo.available && !ticketInfo.alreadyRegistered && isLoggedIn"
|
v-if="
|
||||||
|
ticketInfo.available && !ticketInfo.alreadyRegistered && isLoggedIn
|
||||||
|
"
|
||||||
class="ticket-panel"
|
class="ticket-panel"
|
||||||
>
|
>
|
||||||
<div class="box-title">
|
|
||||||
{{ ticketInfo.isFree ? "Register" : "Purchase Ticket" }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="ticketInfo.isMember && ticketInfo.isFree"
|
v-if="ticketInfo.isMember && ticketInfo.isFree"
|
||||||
class="ticket-notice"
|
class="ticket-notice"
|
||||||
|
|
@ -90,8 +88,7 @@
|
||||||
class="ticket-notice"
|
class="ticket-notice"
|
||||||
style="color: var(--candle)"
|
style="color: var(--candle)"
|
||||||
>
|
>
|
||||||
Payment of {{ ticketInfo.formattedPrice }} will be processed
|
Payment of {{ ticketInfo.formattedPrice }} will be processed securely
|
||||||
securely
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -129,7 +126,7 @@
|
||||||
autocomplete="name"
|
autocomplete="name"
|
||||||
required
|
required
|
||||||
:disabled="processing"
|
:disabled="processing"
|
||||||
>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|
@ -142,7 +139,7 @@
|
||||||
autocomplete="email"
|
autocomplete="email"
|
||||||
required
|
required
|
||||||
:disabled="processing"
|
:disabled="processing"
|
||||||
>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
|
|
@ -160,11 +157,15 @@
|
||||||
v-model="form.createAccount"
|
v-model="form.createAccount"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:disabled="processing"
|
:disabled="processing"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
>Create a free guest account so I can manage my
|
||||||
|
registration</span
|
||||||
>
|
>
|
||||||
<span>Create a free guest account so I can manage my registration</span>
|
|
||||||
</label>
|
</label>
|
||||||
<p class="field-hint consent-hint">
|
<p class="field-hint consent-hint">
|
||||||
Guest accounts let you view your tickets and register faster next time. We won't add you to member communications.
|
Guest accounts let you view your tickets and register faster next
|
||||||
|
time. We won't add you to member communications.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -190,24 +191,18 @@
|
||||||
class="ticket-panel"
|
class="ticket-panel"
|
||||||
>
|
>
|
||||||
<div class="box-title">Waitlist</div>
|
<div class="box-title">Waitlist</div>
|
||||||
<p class="ticket-status" style="color: var(--ember)">
|
<p class="ticket-status" style="color: var(--ember)">Event Sold Out</p>
|
||||||
Event Sold Out
|
|
||||||
</p>
|
|
||||||
<p class="ticket-detail">
|
<p class="ticket-detail">
|
||||||
This event is currently at capacity. Join the waitlist to be notified
|
This event is currently at capacity. Join the waitlist to be notified
|
||||||
if spots become available.
|
if spots become available.
|
||||||
</p>
|
</p>
|
||||||
<button class="btn" @click="handleJoinWaitlist">
|
<button class="btn" @click="handleJoinWaitlist">Join Waitlist</button>
|
||||||
Join Waitlist
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sold Out (No Waitlist) -->
|
<!-- Sold Out (No Waitlist) -->
|
||||||
<div v-else-if="!ticketInfo.available" class="ticket-panel">
|
<div v-else-if="!ticketInfo.available" class="ticket-panel">
|
||||||
<div class="box-title">Tickets</div>
|
<div class="box-title">Tickets</div>
|
||||||
<p class="ticket-status" style="color: var(--ember)">
|
<p class="ticket-status" style="color: var(--ember)">Event Sold Out</p>
|
||||||
Event Sold Out
|
|
||||||
</p>
|
|
||||||
<p class="ticket-detail">
|
<p class="ticket-detail">
|
||||||
Unfortunately, this event is at capacity and no longer accepting
|
Unfortunately, this event is at capacity and no longer accepting
|
||||||
registrations.
|
registrations.
|
||||||
|
|
@ -311,7 +306,9 @@ const fetchTicketInfo = async (emailOverride = null) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regular ticket availability check
|
// Regular ticket availability check
|
||||||
const params = effectiveEmail ? `?email=${encodeURIComponent(effectiveEmail)}` : "";
|
const params = effectiveEmail
|
||||||
|
? `?email=${encodeURIComponent(effectiveEmail)}`
|
||||||
|
: "";
|
||||||
const response = await $fetch(
|
const response = await $fetch(
|
||||||
`/api/events/${props.eventId}/tickets/available${params}`,
|
`/api/events/${props.eventId}/tickets/available${params}`,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,6 @@
|
||||||
<div v-if="event.circle" class="event-meta-item">
|
<div v-if="event.circle" class="event-meta-item">
|
||||||
<CircleBadge :circle="event.circle" />
|
<CircleBadge :circle="event.circle" />
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -85,7 +81,7 @@
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>About This Event</h2>
|
<h2>About This Event</h2>
|
||||||
<p>{{ event.description }}</p>
|
<div class="prose" v-html="renderMarkdown(event.description)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Series Description -->
|
<!-- Series Description -->
|
||||||
|
|
@ -94,17 +90,23 @@
|
||||||
class="section"
|
class="section"
|
||||||
>
|
>
|
||||||
<h2>About the {{ event.series.title }} Series</h2>
|
<h2>About the {{ event.series.title }} Series</h2>
|
||||||
<p>{{ event.series.description }}</p>
|
<div class="prose" v-html="renderMarkdown(event.series.description)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional Information -->
|
||||||
|
<div v-if="event.content" class="section">
|
||||||
|
<h2>Additional Information</h2>
|
||||||
|
<div class="prose" v-html="renderMarkdown(event.content)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Agenda -->
|
<!-- Agenda -->
|
||||||
<div v-if="event.agenda?.length" class="section">
|
<div v-if="event.agenda?.length" class="section">
|
||||||
<h2>Agenda</h2>
|
<h2>Agenda</h2>
|
||||||
<ol class="agenda-list">
|
<ul class="agenda-list">
|
||||||
<li v-for="(item, index) in event.agenda" :key="index">
|
<li v-for="(item, index) in event.agenda" :key="index">
|
||||||
{{ item }}
|
{{ item }}
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Speakers -->
|
<!-- Speakers -->
|
||||||
|
|
@ -143,7 +145,7 @@
|
||||||
<div class="box-title">Event Details</div>
|
<div class="box-title">Event Details</div>
|
||||||
<div v-if="event.eventType" class="detail-row">
|
<div v-if="event.eventType" class="detail-row">
|
||||||
<span class="detail-key">Type</span>
|
<span class="detail-key">Type</span>
|
||||||
<span class="detail-val">{{ event.eventType }}</span>
|
<span class="detail-val">{{ eventTypeLabel(event.eventType) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="event.membersOnly" class="detail-row">
|
<div v-if="event.membersOnly" class="detail-row">
|
||||||
<span class="detail-key">Members only</span>
|
<span class="detail-key">Members only</span>
|
||||||
|
|
@ -169,6 +171,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { eventTypeLabel } from "~/config/eventTypes";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
|
@ -190,6 +194,7 @@ if (error.value?.statusCode === 404) {
|
||||||
|
|
||||||
const { memberData, checkMemberStatus } = useAuth();
|
const { memberData, checkMemberStatus } = useAuth();
|
||||||
const { trackGoal, isComplete } = useOnboarding();
|
const { trackGoal, isComplete } = useOnboarding();
|
||||||
|
const { render: renderMarkdown } = useMarkdown();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await checkMemberStatus();
|
await checkMemberStatus();
|
||||||
|
|
@ -232,16 +237,12 @@ const handleTicketError = (err) => {
|
||||||
console.error("Ticket purchase failed:", err);
|
console.error("Ticket purchase failed:", err);
|
||||||
};
|
};
|
||||||
|
|
||||||
useHead(() => ({
|
useSiteMeta(() => ({
|
||||||
title: event.value
|
title: event.value ? `${event.value.title} · Events` : "Event",
|
||||||
? `${event.value.title} - Ghost Guild Events`
|
description:
|
||||||
: "Event - Ghost Guild",
|
event.value?.description || "View event details and register.",
|
||||||
meta: [
|
type: "article",
|
||||||
{
|
image: event.value?.slug ? `/og/events/${event.value.slug}.png` : undefined,
|
||||||
name: "description",
|
|
||||||
content: event.value?.description || "View event details and register",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -349,12 +350,79 @@ useHead(() => ({
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.section p {
|
.section p {
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
max-width: 560px;
|
max-width: 560px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prose {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.7;
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
.prose :deep(p) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.prose :deep(p:last-child) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.prose :deep(a) {
|
||||||
|
color: var(--ember);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
.prose :deep(strong) {
|
||||||
|
color: var(--text-bright);
|
||||||
|
}
|
||||||
|
.prose :deep(ul),
|
||||||
|
.prose :deep(ol) {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 8px 0 12px;
|
||||||
|
}
|
||||||
|
.prose :deep(ul li),
|
||||||
|
.prose :deep(ol li) {
|
||||||
|
position: relative;
|
||||||
|
padding: 2px 0 2px 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.prose :deep(ul li::before) {
|
||||||
|
content: "›";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 2px;
|
||||||
|
color: var(--candle-faint);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
.prose :deep(ol) {
|
||||||
|
counter-reset: prose-item;
|
||||||
|
}
|
||||||
|
.prose :deep(ol li) {
|
||||||
|
counter-increment: prose-item;
|
||||||
|
padding-left: 28px;
|
||||||
|
}
|
||||||
|
.prose :deep(ol li::before) {
|
||||||
|
content: counter(prose-item) ".";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 2px;
|
||||||
|
color: var(--candle-faint);
|
||||||
|
}
|
||||||
|
.prose :deep(blockquote) {
|
||||||
|
border-left: 2px solid var(--candle-faint);
|
||||||
|
padding-left: 12px;
|
||||||
|
margin: 12px 0;
|
||||||
|
color: var(--text-faint);
|
||||||
|
}
|
||||||
|
.prose :deep(code) {
|
||||||
|
font-family: "Commit Mono", monospace;
|
||||||
|
background: var(--input-bg);
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.circle-badges {
|
.circle-badges {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
|
@ -367,10 +435,27 @@ useHead(() => ({
|
||||||
}
|
}
|
||||||
|
|
||||||
.agenda-list {
|
.agenda-list {
|
||||||
padding-left: 20px;
|
list-style: none;
|
||||||
font-size: 12px;
|
padding: 0;
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 14px;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
line-height: 2;
|
line-height: 1.7;
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
.agenda-list li {
|
||||||
|
position: relative;
|
||||||
|
padding: 2px 0 2px 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.agenda-list li::before {
|
||||||
|
content: "›";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 2px;
|
||||||
|
color: var(--candle-faint);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.speaker {
|
.speaker {
|
||||||
|
|
|
||||||
|
|
@ -45,34 +45,21 @@
|
||||||
<span v-if="event.isCancelled" class="cancelled-tag"
|
<span v-if="event.isCancelled" class="cancelled-tag"
|
||||||
>cancelled</span
|
>cancelled</span
|
||||||
>
|
>
|
||||||
|
<span v-if="event.isRegistered" class="registered-tag"
|
||||||
|
>Registered</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="event.tagline" class="event-tagline">
|
<div v-if="event.tagline" class="event-tagline">
|
||||||
{{ event.tagline }}
|
{{ event.tagline }}
|
||||||
</div>
|
</div>
|
||||||
<div class="event-sub">
|
<div class="event-sub">
|
||||||
<span v-if="event.eventType" class="event-type-tag">{{
|
<span v-if="event.eventType" class="event-type-tag">{{
|
||||||
event.eventType
|
eventTypeLabel(event.eventType)
|
||||||
}}</span>
|
}}</span>
|
||||||
<span v-if="event.eventType" class="sep">·</span>
|
<span v-if="event.eventType" class="sep">·</span>
|
||||||
<span class="event-location">{{ formatLocation(event) }}</span>
|
<span class="event-location">{{ formatLocation(event) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="event-capacity">
|
|
||||||
<template v-if="event.maxAttendees">
|
|
||||||
<span :class="{ 'seats-warn': isAlmostFull(event) }">
|
|
||||||
{{ event.registeredCount || 0 }}/{{ event.maxAttendees }} seats
|
|
||||||
</span>
|
|
||||||
<span v-if="isSoldOut(event)" class="capacity-badge sold-out"
|
|
||||||
>Sold out</span
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-else-if="isAlmostFull(event)"
|
|
||||||
class="capacity-badge limited"
|
|
||||||
>Limited tickets</span
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
<template v-else>Open</template>
|
|
||||||
</span>
|
|
||||||
<div class="event-badges">
|
<div class="event-badges">
|
||||||
<span v-if="event.membersOnly" class="members-badge">Members</span>
|
<span v-if="event.membersOnly" class="members-badge">Members</span>
|
||||||
<CircleBadge v-if="event.circle" :circle="event.circle" />
|
<CircleBadge v-if="event.circle" :circle="event.circle" />
|
||||||
|
|
@ -119,15 +106,20 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { EVENT_TYPES, eventTypeLabel } from "~/config/eventTypes";
|
||||||
|
|
||||||
|
useSiteMeta({
|
||||||
|
title: "Events",
|
||||||
|
description:
|
||||||
|
"Workshops, meetups, and gatherings for game developers practicing cooperative models. Some events are open to the public; others are for Ghost Guild members.",
|
||||||
|
});
|
||||||
|
|
||||||
const activeFilter = ref("all");
|
const activeFilter = ref("all");
|
||||||
const includePastEvents = ref(false);
|
const includePastEvents = ref(false);
|
||||||
|
|
||||||
const filterOptions = [
|
const filterOptions = [
|
||||||
{ label: "All", value: "all" },
|
{ label: "All", value: "all" },
|
||||||
{ label: "Workshops", value: "workshop" },
|
...EVENT_TYPES.map((t) => ({ label: t.label, value: t.value })),
|
||||||
{ label: "Community", value: "community" },
|
|
||||||
{ label: "Social", value: "social" },
|
|
||||||
{ label: "Showcase", value: "showcase" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const { data: eventsData } = await useFetch("/api/events");
|
const { data: eventsData } = await useFetch("/api/events");
|
||||||
|
|
@ -168,6 +160,7 @@ const formatTime = (event) => {
|
||||||
return new Date(event.startDate).toLocaleTimeString("en-US", {
|
return new Date(event.startDate).toLocaleTimeString("en-US", {
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
|
timeZoneName: "short",
|
||||||
timeZone: event.displayTimezone || "America/Toronto",
|
timeZone: event.displayTimezone || "America/Toronto",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -181,16 +174,6 @@ const formatLocation = (event) => {
|
||||||
return event.location;
|
return event.location;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSoldOut = (event) => {
|
|
||||||
if (!event.maxAttendees) return false;
|
|
||||||
return (event.registeredCount || 0) >= event.maxAttendees;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isAlmostFull = (event) => {
|
|
||||||
if (!event.maxAttendees) return false;
|
|
||||||
if (isSoldOut(event)) return false;
|
|
||||||
return (event.registeredCount || 0) / event.maxAttendees >= 0.8;
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -221,7 +204,7 @@ const isAlmostFull = (event) => {
|
||||||
|
|
||||||
.event-row {
|
.event-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 90px 1fr auto auto;
|
grid-template-columns: 90px 1fr auto;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
padding: 14px 0;
|
padding: 14px 0;
|
||||||
|
|
@ -293,6 +276,16 @@ const isAlmostFull = (event) => {
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.registered-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--candle);
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
padding: 1px 5px;
|
||||||
|
line-height: 1.5;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.event-tagline {
|
.event-tagline {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|
@ -321,35 +314,6 @@ const isAlmostFull = (event) => {
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-capacity {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--text-faint);
|
|
||||||
white-space: nowrap;
|
|
||||||
padding-top: 2px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
.seats-warn {
|
|
||||||
color: var(--ember);
|
|
||||||
}
|
|
||||||
.capacity-badge {
|
|
||||||
font-size: 9px;
|
|
||||||
letter-spacing: 0.07em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
padding: 1px 5px;
|
|
||||||
border: 1px dashed currentColor;
|
|
||||||
line-height: 1.5;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.capacity-badge.limited {
|
|
||||||
color: var(--ember);
|
|
||||||
}
|
|
||||||
.capacity-badge.sold-out {
|
|
||||||
color: var(--text-faint);
|
|
||||||
border-style: solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-badges {
|
.event-badges {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -481,7 +445,6 @@ const isAlmostFull = (event) => {
|
||||||
grid-template-columns: 70px 1fr;
|
grid-template-columns: 70px 1fr;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.event-capacity,
|
|
||||||
.event-badges {
|
.event-badges {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,17 +35,30 @@ export default defineEventHandler(async (event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch events from database
|
// Fetch events from database
|
||||||
const events = await Event.find(filter)
|
const events = await Event.find(filter).sort({ startDate: 1 }).lean();
|
||||||
.sort({ startDate: 1 })
|
|
||||||
.select("-registrations") // Don't expose registration details in list view
|
|
||||||
.lean();
|
|
||||||
|
|
||||||
// Add computed fields
|
const requesterEmail = requester?.email?.toLowerCase();
|
||||||
const eventsWithMeta = events.map((event) => ({
|
const requesterId = requester?._id?.toString();
|
||||||
...event,
|
|
||||||
|
// Add computed fields; strip registrations from the response.
|
||||||
|
const eventsWithMeta = events.map((event) => {
|
||||||
|
const regs = event.registrations || [];
|
||||||
|
const isRegistered =
|
||||||
|
!!requester &&
|
||||||
|
regs.some(
|
||||||
|
(r) =>
|
||||||
|
!r.cancelledAt &&
|
||||||
|
((r.memberId && r.memberId.toString() === requesterId) ||
|
||||||
|
(r.email && r.email.toLowerCase() === requesterEmail)),
|
||||||
|
);
|
||||||
|
const { registrations, ...rest } = event;
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
id: event._id.toString(),
|
id: event._id.toString(),
|
||||||
registeredCount: event.registrations?.length || 0,
|
registeredCount: regs.length,
|
||||||
}));
|
isRegistered,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return eventsWithMeta;
|
return eventsWithMeta;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue