ghostguild-org/app/pages/events/index.vue

259 lines
6.9 KiB
Vue

<template>
<div>
<!-- HERO (compact) -->
<div class="hero">
<h1>Events</h1>
<p>Workshops, meetups, and gatherings for game developers practicing cooperative models.</p>
</div>
<!-- FILTER BAR -->
<FilterBar v-model="activeFilter" :filters="filterOptions">
<label class="filter-toggle">
<input type="checkbox" v-model="includePastEvents"> Show past events
</label>
</FilterBar>
<!-- EVENT LIST -->
<div class="event-list-full">
<div
v-for="event in filteredEvents"
:key="event._id"
class="event-row"
>
<span class="event-date">{{ formatDate(event.startDate) }}</span>
<div class="event-info">
<div class="event-title">
<NuxtLink :to="`/events/${event._id}`">{{ event.title }}</NuxtLink>
</div>
<div v-if="event.eventType" class="event-type">{{ event.eventType }}</div>
</div>
<span class="event-capacity">
<template v-if="event.maxAttendees">
<span :class="{ 'seats-warn': isAlmostFull(event) }">
{{ event.registeredCount || 0 }}/{{ event.maxAttendees }} seats
</span>
</template>
<template v-else>Open</template>
</span>
<CircleBadge v-if="event.circle" :circle="event.circle" />
<span v-else class="badge all">All</span>
</div>
<div v-if="!filteredEvents?.length" class="empty">No events found</div>
</div>
<!-- EVENT SERIES -->
<div v-if="activeSeries?.length" class="full-section">
<div class="section-label">Event Series</div>
<div class="series-grid">
<NuxtLink
v-for="series in activeSeries"
:key="series._id"
:to="`/series/${series._id}`"
class="series-box"
>
<h3>{{ series.title }}</h3>
<p class="series-desc">{{ series.description }}</p>
<div class="series-meta">
<span>{{ series.eventCount || series.events?.length || 0 }} sessions</span>
<span v-if="series.startDate">{{ formatDate(series.startDate) }} &ndash; {{ formatDate(series.endDate) }}</span>
</div>
</NuxtLink>
</div>
</div>
<!-- PROPOSE AN EVENT -->
<div class="full-section">
<div class="section-label">Have an idea?</div>
<DashedBox>
<h3>Propose an Event</h3>
<p>Members can propose events for any circle. Workshops, social hangs, talks, or anything else that serves the community.</p>
<NuxtLink to="/events" class="cta">Propose an event &rarr;</NuxtLink>
</DashedBox>
</div>
</div>
</template>
<script setup>
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' },
]
const { data: eventsData } = await useFetch('/api/events')
const { data: seriesData } = await useFetch('/api/series')
const now = new Date()
const filteredEvents = computed(() => {
if (!eventsData.value) return []
return eventsData.value.filter((event) => {
if (!includePastEvents.value && new Date(event.startDate) < now) return false
if (activeFilter.value !== 'all' && event.eventType !== activeFilter.value) return false
return true
})
})
const activeSeries = computed(() => {
if (!seriesData.value) return []
return seriesData.value.filter(
(s) => s.status === 'active' || s.isOngoing || s.isUpcoming,
)
})
const formatDate = (dateStr) => {
if (!dateStr) return ''
const d = new Date(dateStr)
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
}
const isAlmostFull = (event) => {
if (!event.maxAttendees) return false
return (event.registeredCount || 0) / event.maxAttendees > 0.8
}
</script>
<style scoped>
.hero {
padding: 32px 32px 24px;
border-bottom: 1px dashed var(--border);
}
.hero h1 {
font-family: 'Brygada 1918', serif;
font-size: 28px;
font-weight: 600;
color: var(--text-bright);
line-height: 1.2;
margin-bottom: 8px;
}
.hero p {
color: var(--text-dim);
font-size: 13px;
line-height: 1.7;
max-width: 460px;
}
/* ---- EVENT LIST ---- */
.event-list-full {
padding: 0 32px;
border-bottom: 1px dashed var(--border);
}
.event-row {
display: grid;
grid-template-columns: 80px 1fr auto auto;
gap: 16px;
align-items: baseline;
padding: 12px 0;
border-bottom: 1px dashed var(--border);
transition: padding-left 0.2s;
}
.event-row:first-child { padding-top: 16px; }
.event-row:last-child { border-bottom: none; padding-bottom: 16px; }
.event-row:hover { padding-left: 4px; }
.event-date { color: var(--text-faint); font-size: 12px; white-space: nowrap; }
.event-info { min-width: 0; }
.event-title { color: var(--text); font-size: 13px; }
.event-title a { color: var(--text); text-decoration: none; }
.event-title a:hover { color: var(--candle); }
.event-type { font-size: 10px; color: var(--text-faint); margin-top: 1px; }
.event-capacity { font-size: 11px; color: var(--text-faint); white-space: nowrap; }
.seats-warn { color: var(--ember); }
/* ---- FULL SECTION ---- */
.full-section {
padding: 32px;
border-bottom: 1px dashed var(--border);
}
/* ---- SERIES ---- */
.series-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0;
border: 1px dashed var(--border);
}
.series-box {
padding: 20px;
border-right: 1px dashed var(--border);
text-decoration: none;
transition: background 0.15s;
}
.series-box:last-child { border-right: none; }
.series-box:hover { background: var(--surface-hover); }
.series-box h3 {
font-family: 'Brygada 1918', serif;
font-size: 16px;
font-weight: 500;
color: var(--text-bright);
margin-bottom: 4px;
}
.series-desc {
font-size: 12px;
color: var(--text-dim);
line-height: 1.6;
margin-bottom: 8px;
}
.series-meta {
font-size: 10px;
color: var(--text-faint);
display: flex;
gap: 12px;
align-items: center;
}
/* ---- PROPOSE ---- */
.full-section h3 {
font-family: 'Brygada 1918', serif;
font-size: 16px;
font-weight: 500;
color: var(--text-bright);
margin-bottom: 4px;
}
.full-section p {
font-size: 12px;
color: var(--text-dim);
line-height: 1.7;
max-width: 560px;
}
.cta {
display: inline-block;
margin-top: 8px;
font-size: 12px;
color: var(--candle);
}
.filter-toggle {
display: flex;
align-items: center;
gap: 6px;
margin-left: auto;
font-size: 11px;
color: var(--text-faint);
cursor: pointer;
}
.filter-toggle input {
accent-color: var(--candle-dim);
}
.empty {
padding: 24px 0;
color: var(--text-faint);
font-size: 12px;
}
@media (max-width: 768px) {
.event-row {
grid-template-columns: 60px 1fr;
gap: 8px;
}
.event-capacity { display: none; }
.series-grid { grid-template-columns: 1fr; }
.series-box { border-right: none; border-bottom: 1px dashed var(--border); }
.series-box:last-child { border-bottom: none; }
}
</style>