- about.vue: promote h3 → h2 on circle headings (h1→h2→h2→h2) - coming-soon.vue: font-weight 700 → 600 - members/[id].vue: inline circle badge → <CircleBadge/>; hero size 42→36 - community-guidelines.vue: padding + font-size off-scale snaps - board.vue: loading/empty padding 60→64 - series/index.vue, join.vue: padding off-scale snaps
245 lines
5.8 KiB
Vue
245 lines
5.8 KiB
Vue
<template>
|
||
<div>
|
||
<PageHeader
|
||
title="Event Series"
|
||
subtitle="Multi-session events on cooperative topics"
|
||
/>
|
||
|
||
<div v-if="pending" class="state-msg">Loading series...</div>
|
||
|
||
<div v-else-if="!filteredSeries.length" class="state-msg">
|
||
<p>
|
||
No series right now. Check back later or browse
|
||
<NuxtLink to="/events">upcoming events</NuxtLink>.
|
||
</p>
|
||
</div>
|
||
|
||
<div v-else>
|
||
<section
|
||
v-for="series in filteredSeries"
|
||
:key="series.id"
|
||
class="series-section"
|
||
>
|
||
<div class="series-head">
|
||
<h2>{{ series.title }}</h2>
|
||
<div class="series-meta-row">
|
||
<span v-if="series.type" class="badge all">{{ formatSeriesType(series.type) }}</span>
|
||
<span class="meta-text">
|
||
{{ series.eventCount }} sessions<template v-if="series.totalEvents"> of {{ series.totalEvents }} planned</template>
|
||
</span>
|
||
<span v-if="series.startDate && series.endDate" class="meta-text">
|
||
{{ formatDateRange(series.startDate, series.endDate) }}
|
||
</span>
|
||
<span v-if="series.totalRegistrations" class="meta-text">
|
||
{{ series.totalRegistrations }} registered
|
||
</span>
|
||
</div>
|
||
<p v-if="series.description" class="series-desc">
|
||
{{ series.description }}
|
||
</p>
|
||
</div>
|
||
|
||
<div v-if="series.events?.length" class="sessions">
|
||
<div
|
||
v-for="(event, index) in series.events"
|
||
:key="event.id"
|
||
class="event-row"
|
||
>
|
||
<span class="event-num">
|
||
{{ String(event.series?.position || index + 1).padStart(2, '0') }}
|
||
</span>
|
||
<span class="event-date">{{ formatEventDate(event.startDate) }}</span>
|
||
<div class="event-info">
|
||
<NuxtLink
|
||
:to="`/events/${event.slug || event.id}`"
|
||
class="event-title-link"
|
||
>
|
||
{{ event.title }}
|
||
</NuxtLink>
|
||
<span class="event-status">{{ getEventStatus(event) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="series-foot">
|
||
<NuxtLink :to="`/series/${series.id}`" class="view-link">
|
||
View series →
|
||
</NuxtLink>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
useHead({
|
||
title: "Event Series - Ghost Guild",
|
||
meta: [
|
||
{
|
||
name: "description",
|
||
content:
|
||
"Multi-session events on cooperative topics for game developers.",
|
||
},
|
||
],
|
||
});
|
||
|
||
const { data: seriesData, pending } = await useFetch("/api/series", {
|
||
query: { includeHidden: false },
|
||
});
|
||
|
||
const filteredSeries = computed(() => {
|
||
if (!seriesData.value) return [];
|
||
return seriesData.value.filter(
|
||
(series) => series.status === "active" || series.status === "upcoming",
|
||
);
|
||
});
|
||
|
||
const formatSeriesType = (type) => {
|
||
const types = {
|
||
workshop_series: "Workshop Series",
|
||
recurring_meetup: "Recurring Meetup",
|
||
multi_day: "Multi-Day Event",
|
||
course: "Course",
|
||
tournament: "Tournament",
|
||
};
|
||
return types[type] || type;
|
||
};
|
||
|
||
const formatEventDate = (date) => {
|
||
return new Date(date).toLocaleDateString("en-US", {
|
||
month: "short",
|
||
day: "numeric",
|
||
year: "numeric",
|
||
});
|
||
};
|
||
|
||
const formatDateRange = (startDate, endDate) => {
|
||
const start = new Date(startDate);
|
||
const end = new Date(endDate);
|
||
const formatter = new Intl.DateTimeFormat("en-US", {
|
||
month: "short",
|
||
day: "numeric",
|
||
year: "numeric",
|
||
});
|
||
return `${formatter.format(start)} – ${formatter.format(end)}`;
|
||
};
|
||
|
||
const getEventStatus = (event) => {
|
||
const now = new Date();
|
||
const startDate = new Date(event.startDate);
|
||
const endDate = new Date(event.endDate);
|
||
if (now < startDate) return "Upcoming";
|
||
if (now >= startDate && now <= endDate) return "Ongoing";
|
||
return "Completed";
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.state-msg {
|
||
padding: 32px 28px;
|
||
color: var(--text-dim);
|
||
font-size: 12px;
|
||
}
|
||
.state-msg p { max-width: 560px; }
|
||
|
||
.series-section {
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
|
||
.series-head {
|
||
padding: 24px 28px 16px;
|
||
}
|
||
.series-head h2 {
|
||
font-family: var(--font-display);
|
||
font-size: 22px;
|
||
font-weight: 600;
|
||
color: var(--text-bright);
|
||
line-height: 1.2;
|
||
margin-bottom: 10px;
|
||
}
|
||
.series-meta-row {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 10px;
|
||
font-size: 12px;
|
||
}
|
||
.meta-text {
|
||
color: var(--text-faint);
|
||
font-size: 12px;
|
||
}
|
||
.series-desc {
|
||
font-size: 13px;
|
||
color: var(--text-dim);
|
||
line-height: 1.7;
|
||
max-width: 640px;
|
||
margin: 0;
|
||
}
|
||
|
||
.sessions {
|
||
border-top: 1px dashed var(--border);
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
.event-row {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 12px;
|
||
padding: 12px 28px;
|
||
border-bottom: 1px dashed var(--border);
|
||
font-size: 12px;
|
||
}
|
||
.event-row:last-child { border-bottom: none; }
|
||
.event-num {
|
||
flex: 0 0 24px;
|
||
color: var(--text-faint);
|
||
font-size: 11px;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.event-date {
|
||
flex: 0 0 110px;
|
||
color: var(--text-faint);
|
||
white-space: nowrap;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.event-info { flex: 1 1 0; min-width: 0; }
|
||
.event-title-link {
|
||
color: var(--text);
|
||
text-decoration: none;
|
||
font-size: 13px;
|
||
}
|
||
.event-title-link:hover { color: var(--candle); }
|
||
.event-status {
|
||
font-size: 10px;
|
||
color: var(--text-faint);
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.series-foot {
|
||
padding: 14px 28px 24px;
|
||
}
|
||
.view-link {
|
||
font-size: 12px;
|
||
color: var(--candle);
|
||
letter-spacing: 0.04em;
|
||
}
|
||
.view-link:hover { text-decoration: underline; }
|
||
|
||
@media (max-width: 768px) {
|
||
.series-head,
|
||
.series-foot {
|
||
padding-left: 20px;
|
||
padding-right: 20px;
|
||
}
|
||
.event-row {
|
||
flex-wrap: wrap;
|
||
padding-left: 20px;
|
||
padding-right: 20px;
|
||
row-gap: 2px;
|
||
}
|
||
.event-info {
|
||
flex-basis: 100%;
|
||
margin-left: 36px;
|
||
}
|
||
}
|
||
</style>
|