259 lines
6.9 KiB
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) }} – {{ 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 →</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>
|