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:
Jennie Robinson Faber 2026-05-21 17:50:48 +01:00
parent 2ffaf0ef09
commit 622cc8e53b
4 changed files with 176 additions and 118 deletions

View file

@ -45,34 +45,21 @@
<span v-if="event.isCancelled" class="cancelled-tag"
>cancelled</span
>
<span v-if="event.isRegistered" class="registered-tag"
>Registered</span
>
</div>
<div v-if="event.tagline" class="event-tagline">
{{ event.tagline }}
</div>
<div class="event-sub">
<span v-if="event.eventType" class="event-type-tag">{{
event.eventType
eventTypeLabel(event.eventType)
}}</span>
<span v-if="event.eventType" class="sep">·</span>
<span class="event-location">{{ formatLocation(event) }}</span>
</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">
<span v-if="event.membersOnly" class="members-badge">Members</span>
<CircleBadge v-if="event.circle" :circle="event.circle" />
@ -119,15 +106,20 @@
</template>
<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 includePastEvents = ref(false);
const filterOptions = [
{ label: "All", value: "all" },
{ label: "Workshops", value: "workshop" },
{ label: "Community", value: "community" },
{ label: "Social", value: "social" },
{ label: "Showcase", value: "showcase" },
...EVENT_TYPES.map((t) => ({ label: t.label, value: t.value })),
];
const { data: eventsData } = await useFetch("/api/events");
@ -168,6 +160,7 @@ const formatTime = (event) => {
return new Date(event.startDate).toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
timeZoneName: "short",
timeZone: event.displayTimezone || "America/Toronto",
});
};
@ -181,16 +174,6 @@ const formatLocation = (event) => {
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>
<style scoped>
@ -221,7 +204,7 @@ const isAlmostFull = (event) => {
.event-row {
display: grid;
grid-template-columns: 90px 1fr auto auto;
grid-template-columns: 90px 1fr auto;
gap: 16px;
align-items: start;
padding: 14px 0;
@ -293,6 +276,16 @@ const isAlmostFull = (event) => {
line-height: 1.5;
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 {
font-size: 11px;
@ -321,35 +314,6 @@ const isAlmostFull = (event) => {
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 {
display: flex;
flex-direction: column;
@ -481,7 +445,6 @@ const isAlmostFull = (event) => {
grid-template-columns: 70px 1fr;
gap: 8px;
}
.event-capacity,
.event-badges {
display: none;
}