refactor(events): expand eventType taxonomy with central config

Replaces the four-value enum (community/workshop/social/showcase) with
seven values: talk, workshop, community-meetup, coworking, peer-session,
skills-share, info-session. Default is now community-meetup.

Adds app/config/eventTypes.js as the single source of truth for value→label
mapping. Updates the model enum, seed scripts, and admin event list/filter
+ admin dashboard to read from it via EVENT_TYPES and eventTypeLabel().
This commit is contained in:
Jennie Robinson Faber 2026-05-21 17:50:40 +01:00
parent 31144617d7
commit 2ffaf0ef09
6 changed files with 54 additions and 24 deletions

21
app/config/eventTypes.js Normal file
View file

@ -0,0 +1,21 @@
// Central configuration for Ghost Guild event types.
// Keep values in sync with the `eventType` enum in server/models/event.js.
export const EVENT_TYPES = [
{ value: "talk", label: "Talk / Presentation" },
{ value: "workshop", label: "Workshop" },
{ value: "community-meetup", label: "Community Meetup" },
{ value: "coworking", label: "Co-working Session" },
{ value: "peer-session", label: "Peer Session" },
{ value: "skills-share", label: "Skills Share" },
{ value: "info-session", label: "Info Session" },
];
export const EVENT_TYPE_VALUES = EVENT_TYPES.map((t) => t.value);
const labelLookup = Object.fromEntries(
EVENT_TYPES.map((t) => [t.value, t.label]),
);
export function eventTypeLabel(value) {
return labelLookup[value] || value || "";
}

View file

@ -16,15 +16,12 @@
<!-- Filters --> <!-- Filters -->
<div class="filter-bar"> <div class="filter-bar">
<div class="field" style="margin-bottom: 0; flex: 1;"> <div class="field" style="margin-bottom: 0; flex: 1;">
<input v-model="searchQuery" placeholder="Search events..." /> <input v-model="searchQuery" placeholder="Search events..." >
</div> </div>
<div class="field" style="margin-bottom: 0;"> <div class="field" style="margin-bottom: 0;">
<select v-model="typeFilter"> <select v-model="typeFilter">
<option value="all">All Types</option> <option value="all">All Types</option>
<option value="community">Community</option> <option v-for="t in EVENT_TYPES" :key="t.value" :value="t.value">{{ t.label }}</option>
<option value="workshop">Workshop</option>
<option value="social">Social</option>
<option value="showcase">Showcase</option>
</select> </select>
</div> </div>
<div class="field" style="margin-bottom: 0;"> <div class="field" style="margin-bottom: 0;">
@ -71,7 +68,7 @@
<td class="col-title"> <td class="col-title">
<div class="event-title-cell"> <div class="event-title-cell">
<div v-if="event.featureImage?.url && !event.featureImage?.publicId" class="event-thumb"> <div v-if="event.featureImage?.url && !event.featureImage?.publicId" class="event-thumb">
<img :src="event.featureImage.url" :alt="event.title" @error="handleImageError($event)" /> <img :src="event.featureImage.url" :alt="event.title" @error="handleImageError($event)" >
</div> </div>
<div> <div>
<span class="event-name">{{ event.title }}</span> <span class="event-name">{{ event.title }}</span>
@ -89,7 +86,7 @@
</div> </div>
</td> </td>
<td> <td>
<span class="badge" :class="event.eventType">{{ event.eventType }}</span> <span class="badge" :class="event.eventType">{{ eventTypeLabel(event.eventType) }}</span>
</td> </td>
<td class="col-date"> <td class="col-date">
<span class="date-main">{{ formatDate(event) }}</span> <span class="date-main">{{ formatDate(event) }}</span>
@ -128,9 +125,9 @@
</td> </td>
<td class="col-actions"> <td class="col-actions">
<NuxtLink :to="`/events/${event.slug || String(event._id)}`" class="link-btn" title="View">View</NuxtLink> <NuxtLink :to="`/events/${event.slug || String(event._id)}`" class="link-btn" title="View">View</NuxtLink>
<button @click="editEvent(event)" class="link-btn" title="Edit">Edit</button> <button class="link-btn" title="Edit" @click="editEvent(event)">Edit</button>
<button @click="duplicateEvent(event)" class="link-btn" title="Duplicate">Dup</button> <button class="link-btn" title="Duplicate" @click="duplicateEvent(event)">Dup</button>
<button @click="deleteEvent(event)" class="link-btn link-btn-danger" title="Delete">Del</button> <button class="link-btn link-btn-danger" title="Delete" @click="deleteEvent(event)">Del</button>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -169,7 +166,7 @@
<td class="col-title"> <td class="col-title">
<div class="event-title-cell"> <div class="event-title-cell">
<div v-if="event.featureImage?.url && !event.featureImage?.publicId" class="event-thumb"> <div v-if="event.featureImage?.url && !event.featureImage?.publicId" class="event-thumb">
<img :src="event.featureImage.url" :alt="event.title" @error="handleImageError($event)" /> <img :src="event.featureImage.url" :alt="event.title" @error="handleImageError($event)" >
</div> </div>
<div> <div>
<span class="event-name">{{ event.title }}</span> <span class="event-name">{{ event.title }}</span>
@ -187,7 +184,7 @@
</div> </div>
</td> </td>
<td> <td>
<span class="badge" :class="event.eventType">{{ event.eventType }}</span> <span class="badge" :class="event.eventType">{{ eventTypeLabel(event.eventType) }}</span>
</td> </td>
<td class="col-date"> <td class="col-date">
<span class="date-main">{{ formatDate(event) }}</span> <span class="date-main">{{ formatDate(event) }}</span>
@ -226,9 +223,9 @@
</td> </td>
<td class="col-actions"> <td class="col-actions">
<NuxtLink :to="`/events/${event.slug || String(event._id)}`" class="link-btn" title="View">View</NuxtLink> <NuxtLink :to="`/events/${event.slug || String(event._id)}`" class="link-btn" title="View">View</NuxtLink>
<button @click="editEvent(event)" class="link-btn" title="Edit">Edit</button> <button class="link-btn" title="Edit" @click="editEvent(event)">Edit</button>
<button @click="duplicateEvent(event)" class="link-btn" title="Duplicate">Dup</button> <button class="link-btn" title="Duplicate" @click="duplicateEvent(event)">Dup</button>
<button @click="deleteEvent(event)" class="link-btn link-btn-danger" title="Delete">Del</button> <button class="link-btn link-btn-danger" title="Delete" @click="deleteEvent(event)">Del</button>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -267,6 +264,8 @@
</template> </template>
<script setup> <script setup>
import { EVENT_TYPES, eventTypeLabel } from '~/config/eventTypes'
definePageMeta({ definePageMeta({
layout: 'admin', layout: 'admin',
middleware: 'admin', middleware: 'admin',

View file

@ -91,7 +91,7 @@
<span class="item-sub">{{ formatDateTime(event.startDate) }}</span> <span class="item-sub">{{ formatDateTime(event.startDate) }}</span>
</div> </div>
<div class="item-meta"> <div class="item-meta">
<span class="badge" :class="event.eventType">{{ event.eventType }}</span> <span class="badge" :class="event.eventType">{{ eventTypeLabel(event.eventType) }}</span>
<span class="item-date">{{ event.location || 'Online' }}</span> <span class="item-date">{{ event.location || 'Online' }}</span>
</div> </div>
</div> </div>
@ -106,6 +106,8 @@
</template> </template>
<script setup> <script setup>
import { eventTypeLabel } from '~/config/eventTypes'
definePageMeta({ definePageMeta({
layout: 'admin', layout: 'admin',
middleware: 'admin', middleware: 'admin',

View file

@ -32,7 +32,7 @@ const sampleEvents = [
content: 'This informal meetup is perfect for connecting with other developers interested in cooperative business models. We\'ll have brief presentations, open discussions, and time for networking.\n\nAgenda:\n- Welcome & introductions\n- Member spotlight presentations\n- Open discussion on cooperative challenges and successes\n- Networking and social time', content: 'This informal meetup is perfect for connecting with other developers interested in cooperative business models. We\'ll have brief presentations, open discussions, and time for networking.\n\nAgenda:\n- Welcome & introductions\n- Member spotlight presentations\n- Open discussion on cooperative challenges and successes\n- Networking and social time',
startDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7, 19, 0), startDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7, 19, 0),
endDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7, 21, 0), endDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7, 21, 0),
eventType: 'community', eventType: 'community-meetup',
location: '#general', location: '#general',
isOnline: true, isOnline: true,
membersOnly: false, membersOnly: false,
@ -107,7 +107,7 @@ const sampleEvents = [
content: 'Our quarterly showcase featuring presentations from Ghost Guild member studios. Learn about ongoing projects, cooperative development processes, and the unique challenges and benefits of collaborative game creation.\n\nFeatured presentations:\n- "Collaborative Level Design in Practice"\n- "Democratic Decision Making in Creative Projects"\n- "Balancing Individual Creativity with Group Consensus"\n- Q&A with presenting studios', content: 'Our quarterly showcase featuring presentations from Ghost Guild member studios. Learn about ongoing projects, cooperative development processes, and the unique challenges and benefits of collaborative game creation.\n\nFeatured presentations:\n- "Collaborative Level Design in Practice"\n- "Democratic Decision Making in Creative Projects"\n- "Balancing Individual Creativity with Group Consensus"\n- Q&A with presenting studios',
startDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 21, 18, 30), startDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 21, 18, 30),
endDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 21, 21, 0), endDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 21, 21, 0),
eventType: 'showcase', eventType: 'skills-share',
location: '#showcase', location: '#showcase',
isOnline: true, isOnline: true,
membersOnly: true, membersOnly: true,
@ -134,7 +134,7 @@ const sampleEvents = [
content: 'Join us for a casual evening of celebration, networking, and community building. Perfect for new members to meet the community and for existing members to catch up.\n\nActivities:\n- Welcome reception\n- Casual networking\n- Community achievements celebration\n- Light refreshments provided\n- Optional lightning talks (5 min, informal)', content: 'Join us for a casual evening of celebration, networking, and community building. Perfect for new members to meet the community and for existing members to catch up.\n\nActivities:\n- Welcome reception\n- Casual networking\n- Community achievements celebration\n- Light refreshments provided\n- Optional lightning talks (5 min, informal)',
startDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3, 18, 0), startDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3, 18, 0),
endDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3, 21, 0), endDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3, 21, 0),
eventType: 'social', eventType: 'community-meetup',
location: '#social', location: '#social',
isOnline: true, isOnline: true,
membersOnly: true, membersOnly: true,
@ -234,7 +234,7 @@ const sampleEvents = [
content: 'Our February meetup has been cancelled but will be rescheduled soon. Stay tuned for updates!', content: 'Our February meetup has been cancelled but will be rescheduled soon. Stay tuned for updates!',
startDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 42, 19, 0), startDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 42, 19, 0),
endDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 42, 21, 0), endDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 42, 21, 0),
eventType: 'community', eventType: 'community-meetup',
location: '#general', location: '#general',
isOnline: true, isOnline: true,
membersOnly: false, membersOnly: false,

View file

@ -149,7 +149,7 @@ async function seedSeriesEvents() {
"Our monthly community meetup provides a relaxed environment to share your projects, get feedback, and connect with other developers interested in cooperative models.", "Our monthly community meetup provides a relaxed environment to share your projects, get feedback, and connect with other developers interested in cooperative models.",
startDate: new Date("2024-10-12T18:00:00.000Z"), startDate: new Date("2024-10-12T18:00:00.000Z"),
endDate: new Date("2024-10-12T20:00:00.000Z"), endDate: new Date("2024-10-12T20:00:00.000Z"),
eventType: "community", eventType: "community-meetup",
location: "#community-meetup", location: "#community-meetup",
isOnline: true, isOnline: true,
membersOnly: false, membersOnly: false,
@ -176,7 +176,7 @@ async function seedSeriesEvents() {
"Our monthly community meetup provides a relaxed environment to share your projects, get feedback, and connect with other developers interested in cooperative models.", "Our monthly community meetup provides a relaxed environment to share your projects, get feedback, and connect with other developers interested in cooperative models.",
startDate: new Date("2024-11-09T18:00:00.000Z"), startDate: new Date("2024-11-09T18:00:00.000Z"),
endDate: new Date("2024-11-09T20:00:00.000Z"), endDate: new Date("2024-11-09T20:00:00.000Z"),
eventType: "community", eventType: "community-meetup",
location: "#community-meetup", location: "#community-meetup",
isOnline: true, isOnline: true,
membersOnly: false, membersOnly: false,

View file

@ -15,8 +15,16 @@ const eventSchema = new mongoose.Schema({
endDate: { type: Date, required: true }, endDate: { type: Date, required: true },
eventType: { eventType: {
type: String, type: String,
enum: ["community", "workshop", "social", "showcase"], enum: [
default: "community", "talk",
"workshop",
"community-meetup",
"coworking",
"peer-session",
"skills-share",
"info-session",
],
default: "community-meetup",
}, },
// IANA timezone for interpreting datetime input and rendering the event time. // IANA timezone for interpreting datetime input and rendering the event time.
displayTimezone: { type: String, default: "America/Toronto" }, displayTimezone: { type: String, default: "America/Toronto" },