Adding features

This commit is contained in:
Jennie Robinson Faber 2025-10-05 16:15:09 +01:00
parent 600fef2b7c
commit 2b55ca4104
75 changed files with 9796 additions and 2759 deletions

View file

@ -1,4 +1,4 @@
import mongoose from 'mongoose'
import mongoose from "mongoose";
const eventSchema = new mongoose.Schema({
title: { type: String, required: true },
@ -9,14 +9,14 @@ const eventSchema = new mongoose.Schema({
featureImage: {
url: String, // Cloudinary URL
publicId: String, // Cloudinary public ID for transformations
alt: String // Alt text for accessibility
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'
enum: ["community", "workshop", "social", "showcase"],
default: "community",
},
// Online-first location handling
location: {
@ -24,14 +24,15 @@ const eventSchema = new mongoose.Schema({
required: true,
// This will typically be a Slack channel or video conference link
validate: {
validator: function(v) {
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 #)'
}
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
@ -46,97 +47,110 @@ const eventSchema = new mongoose.Schema({
description: String, // Series description
type: {
type: String,
enum: ['workshop_series', 'recurring_meetup', 'multi_day', 'course', 'tournament'],
default: 'workshop_series'
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
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 }
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' },
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
}
earlyBirdDeadline: Date,
},
},
// Circle targeting
targetCircles: [{
type: String,
enum: ['community', 'founder', 'practitioner'],
required: false
}],
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: [{
name: String,
email: String,
membershipLevel: String,
isMember: { type: Boolean, default: false },
paymentStatus: {
type: String,
enum: ['pending', 'completed', 'failed', 'not_required'],
default: 'not_required'
speakers: [
{
name: String,
role: String,
bio: String,
},
paymentId: String, // Helcim transaction ID
amountPaid: { type: Number, default: 0 },
registeredAt: { type: Date, default: Date.now }
}],
],
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 }
})
updatedAt: { type: Date, default: Date.now },
});
// Generate slug from title
function generateSlug(title) {
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
// Pre-save hook to generate slug
eventSchema.pre('save', async function(next) {
eventSchema.pre("save", async function (next) {
try {
if (this.isNew || this.isModified('title')) {
let baseSlug = generateSlug(this.title)
let slug = baseSlug
let counter = 1
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++
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)
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);

View file

@ -1,46 +1,125 @@
// server/models/member.js
import mongoose from 'mongoose'
import { resolve } from 'path'
import { fileURLToPath } from 'url'
import mongoose from "mongoose";
import { resolve } from "path";
import { fileURLToPath } from "url";
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const __dirname = fileURLToPath(new URL(".", import.meta.url));
// Import configs using dynamic imports to avoid build issues
const getValidCircleValues = () => ['community', 'founder', 'practitioner']
const getValidContributionValues = () => ['0', '5', '15', '30', '50']
const getValidCircleValues = () => ["community", "founder", "practitioner"];
const getValidContributionValues = () => ["0", "5", "15", "30", "50"];
const memberSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
name: { type: String, required: true },
circle: {
type: String,
circle: {
type: String,
enum: getValidCircleValues(),
required: true
required: true,
},
contributionTier: {
type: String,
enum: getValidContributionValues(),
required: true
required: true,
},
status: {
type: String,
enum: ['pending_payment', 'active', 'suspended', 'cancelled'],
default: 'pending_payment'
enum: ["pending_payment", "active", "suspended", "cancelled"],
default: "pending_payment",
},
helcimCustomerId: String,
helcimSubscriptionId: String,
paymentMethod: {
type: String,
enum: ['card', 'bank', 'none'],
default: 'none'
enum: ["card", "bank", "none"],
default: "none",
},
subscriptionStartDate: Date,
subscriptionEndDate: Date,
nextBillingDate: Date,
slackInvited: { type: Boolean, default: false },
slackInviteStatus: {
type: String,
enum: ["pending", "sent", "failed", "accepted"],
default: "pending",
},
slackUserId: String,
// Profile fields
pronouns: String,
timeZone: String,
avatar: String,
studio: String,
bio: String,
skills: [String],
location: String,
socialLinks: {
mastodon: String,
linkedin: String,
website: String,
other: String,
},
offering: String,
lookingFor: String,
showInDirectory: { type: Boolean, default: true },
// Privacy settings for profile fields
privacy: {
pronouns: {
type: String,
enum: ["public", "members", "private"],
default: "members",
},
timeZone: {
type: String,
enum: ["public", "members", "private"],
default: "members",
},
avatar: {
type: String,
enum: ["public", "members", "private"],
default: "public",
},
studio: {
type: String,
enum: ["public", "members", "private"],
default: "members",
},
bio: {
type: String,
enum: ["public", "members", "private"],
default: "members",
},
skills: {
type: String,
enum: ["public", "members", "private"],
default: "members",
},
location: {
type: String,
enum: ["public", "members", "private"],
default: "members",
},
socialLinks: {
type: String,
enum: ["public", "members", "private"],
default: "members",
},
offering: {
type: String,
enum: ["public", "members", "private"],
default: "members",
},
lookingFor: {
type: String,
enum: ["public", "members", "private"],
default: "members",
},
},
createdAt: { type: Date, default: Date.now },
lastLogin: Date
})
lastLogin: Date,
});
// Check if model already exists to prevent re-compilation in development
export default mongoose.models.Member || mongoose.model('Member', memberSchema)
export default mongoose.models.Member || mongoose.model("Member", memberSchema);

50
server/models/update.js Normal file
View file

@ -0,0 +1,50 @@
import mongoose from "mongoose";
const updateSchema = new mongoose.Schema({
author: {
type: mongoose.Schema.Types.ObjectId,
ref: "Member",
required: true,
},
content: {
type: String,
required: true,
},
images: [
{
url: String,
publicId: String,
alt: String,
},
],
privacy: {
type: String,
enum: ["public", "members", "private"],
default: "members",
},
commentsEnabled: {
type: Boolean,
default: true,
},
createdAt: {
type: Date,
default: Date.now,
},
updatedAt: {
type: Date,
default: Date.now,
},
});
// Update the updatedAt timestamp on save
updateSchema.pre("save", function (next) {
this.updatedAt = Date.now();
next();
});
// Indexes for performance
updateSchema.index({ createdAt: -1 }); // For sorting by date
updateSchema.index({ privacy: 1, createdAt: -1 }); // Compound index for filtering and sorting
updateSchema.index({ author: 1 }); // For author lookups
export default mongoose.models.Update || mongoose.model("Update", updateSchema);