156 lines
4.8 KiB
JavaScript
156 lines
4.8 KiB
JavaScript
import mongoose from "mongoose";
|
|
|
|
const eventSchema = new mongoose.Schema({
|
|
title: { type: String, required: true },
|
|
slug: { type: String, required: true, unique: true },
|
|
tagline: String,
|
|
description: { type: String, required: true },
|
|
content: String,
|
|
featureImage: {
|
|
url: String, // Cloudinary URL
|
|
publicId: String, // Cloudinary public ID for transformations
|
|
alt: String, // Alt text for accessibility
|
|
},
|
|
startDate: { type: Date, required: true },
|
|
endDate: { type: Date, required: true },
|
|
eventType: {
|
|
type: String,
|
|
enum: ["community", "workshop", "social", "showcase"],
|
|
default: "community",
|
|
},
|
|
// Online-first location handling
|
|
location: {
|
|
type: String,
|
|
required: true,
|
|
// This will typically be a Slack channel or video conference link
|
|
validate: {
|
|
validator: function (v) {
|
|
// Must be either a valid URL or a Slack channel reference
|
|
const urlPattern = /^https?:\/\/.+/;
|
|
const slackPattern = /^#[a-zA-Z0-9-_]+$/;
|
|
return urlPattern.test(v) || slackPattern.test(v);
|
|
},
|
|
message:
|
|
"Location must be a valid URL (video conference link) or Slack channel (starting with #)",
|
|
},
|
|
},
|
|
isOnline: { type: Boolean, default: true }, // Default to online-first
|
|
// Visibility and status controls
|
|
isVisible: { type: Boolean, default: true }, // Hide from public calendar when false
|
|
isCancelled: { type: Boolean, default: false },
|
|
cancellationMessage: String, // Custom message for cancelled events
|
|
membersOnly: { type: Boolean, default: false },
|
|
// Series information - embedded approach for better performance
|
|
series: {
|
|
id: String, // Simple string ID to group related events
|
|
title: String, // Series title (e.g., "Cooperative Game Development Workshop Series")
|
|
description: String, // Series description
|
|
type: {
|
|
type: String,
|
|
enum: [
|
|
"workshop_series",
|
|
"recurring_meetup",
|
|
"multi_day",
|
|
"course",
|
|
"tournament",
|
|
],
|
|
default: "workshop_series",
|
|
},
|
|
position: Number, // Order within the series (e.g., 1 = first event, 2 = second, etc.)
|
|
totalEvents: Number, // Total planned events in the series
|
|
isSeriesEvent: { type: Boolean, default: false }, // Flag to identify series events
|
|
},
|
|
// Event pricing for public attendees (deprecated - use tickets instead)
|
|
pricing: {
|
|
isFree: { type: Boolean, default: true },
|
|
publicPrice: { type: Number, default: 0 }, // Price for non-members
|
|
currency: { type: String, default: "CAD" },
|
|
paymentRequired: { type: Boolean, default: false },
|
|
},
|
|
// Ticket configuration
|
|
tickets: {
|
|
enabled: { type: Boolean, default: false },
|
|
public: {
|
|
available: { type: Boolean, default: false },
|
|
name: { type: String, default: "Public Ticket" },
|
|
description: String,
|
|
price: { type: Number, default: 0 },
|
|
quantity: Number, // null = unlimited
|
|
sold: { type: Number, default: 0 },
|
|
earlyBirdPrice: Number,
|
|
earlyBirdDeadline: Date,
|
|
},
|
|
},
|
|
// Circle targeting
|
|
targetCircles: [
|
|
{
|
|
type: String,
|
|
enum: ["community", "founder", "practitioner"],
|
|
required: false,
|
|
},
|
|
],
|
|
maxAttendees: Number,
|
|
registrationRequired: { type: Boolean, default: false },
|
|
registrationDeadline: Date,
|
|
agenda: [String],
|
|
speakers: [
|
|
{
|
|
name: String,
|
|
role: String,
|
|
bio: String,
|
|
},
|
|
],
|
|
registrations: [
|
|
{
|
|
memberId: { type: mongoose.Schema.Types.ObjectId, ref: "Member" }, // Reference to Member model
|
|
name: String,
|
|
email: String,
|
|
membershipLevel: String,
|
|
isMember: { type: Boolean, default: false },
|
|
paymentStatus: {
|
|
type: String,
|
|
enum: ["pending", "completed", "failed", "not_required"],
|
|
default: "not_required",
|
|
},
|
|
paymentId: String, // Helcim transaction ID
|
|
amountPaid: { type: Number, default: 0 },
|
|
registeredAt: { type: Date, default: Date.now },
|
|
},
|
|
],
|
|
createdBy: { type: String, required: true },
|
|
createdAt: { type: Date, default: Date.now },
|
|
updatedAt: { type: Date, default: Date.now },
|
|
});
|
|
|
|
// Generate slug from title
|
|
function generateSlug(title) {
|
|
return title
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "");
|
|
}
|
|
|
|
// Pre-save hook to generate slug
|
|
eventSchema.pre("save", async function (next) {
|
|
try {
|
|
if (this.isNew || this.isModified("title")) {
|
|
let baseSlug = generateSlug(this.title);
|
|
let slug = baseSlug;
|
|
let counter = 1;
|
|
|
|
// Ensure slug is unique
|
|
while (await this.constructor.findOne({ slug, _id: { $ne: this._id } })) {
|
|
slug = `${baseSlug}-${counter}`;
|
|
counter++;
|
|
}
|
|
|
|
this.slug = slug;
|
|
}
|
|
next();
|
|
} catch (error) {
|
|
console.error("Error in pre-save hook:", error);
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
export default mongoose.models.Event || mongoose.model("Event", eventSchema);
|