tractatus/src/models/NewsletterSubscription.model.js
TheFlow 29fa3956f9 feat: newsletter modal and deployment script enhancements
**Newsletter Modal Implementation**:
- Added modal subscription forms to blog pages
- Improved UX with dedicated modal instead of anchor links
- Location: public/blog.html, public/blog-post.html

**Blog JavaScript Enhancements**:
- Enhanced blog.js and blog-post.js with modal handling
- Newsletter form submission logic
- Location: public/js/blog.js, public/js/blog-post.js

**Deployment Script Improvements**:
- Added pre-deployment checks (server running, version parameters)
- Enhanced visual feedback with status indicators (✓/✗/⚠)
- Version parameter staleness detection
- Location: scripts/deploy-full-project-SAFE.sh

**Demo Page Cleanup**:
- Minor refinements to demo pages
- Location: public/demos/*.html

**Routes Enhancement**:
- Newsletter route additions
- Location: src/routes/index.js

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 13:11:46 +13:00

235 lines
5.9 KiB
JavaScript

/**
* Newsletter Subscription Model
* Manages email subscriptions for blog updates and framework news
*/
const { ObjectId } = require('mongodb');
const { getCollection } = require('../utils/db.util');
class NewsletterSubscription {
/**
* Subscribe a new email address
*/
static async subscribe(data) {
const collection = await getCollection('newsletter_subscriptions');
// Check if already subscribed
const existing = await collection.findOne({
email: data.email.toLowerCase()
});
if (existing) {
// If previously unsubscribed, reactivate
if (!existing.active) {
await collection.updateOne(
{ _id: existing._id },
{
$set: {
active: true,
resubscribed_at: new Date(),
updated_at: new Date()
}
}
);
return { ...existing, active: true };
}
// Already subscribed and active
return existing;
}
// Create new subscription
const subscription = {
email: data.email.toLowerCase(),
name: data.name || null,
source: data.source || 'blog', // blog/homepage/docs/etc
interests: data.interests || [], // e.g., ['framework-updates', 'case-studies', 'research']
verification_token: this._generateToken(),
verified: false, // Will be true after email verification
active: true,
subscribed_at: new Date(),
created_at: new Date(),
updated_at: new Date(),
metadata: {
ip_address: data.ip_address || null,
user_agent: data.user_agent || null,
referrer: data.referrer || null
}
};
const result = await collection.insertOne(subscription);
return { ...subscription, _id: result.insertedId };
}
/**
* Verify email subscription
*/
static async verify(token) {
const collection = await getCollection('newsletter_subscriptions');
const result = await collection.updateOne(
{ verification_token: token, active: true },
{
$set: {
verified: true,
verified_at: new Date(),
updated_at: new Date()
}
}
);
return result.modifiedCount > 0;
}
/**
* Unsubscribe an email address
*/
static async unsubscribe(email, token = null) {
const collection = await getCollection('newsletter_subscriptions');
const filter = token
? { verification_token: token }
: { email: email.toLowerCase() };
const result = await collection.updateOne(
filter,
{
$set: {
active: false,
unsubscribed_at: new Date(),
updated_at: new Date()
}
}
);
return result.modifiedCount > 0;
}
/**
* Find subscription by email
*/
static async findByEmail(email) {
const collection = await getCollection('newsletter_subscriptions');
return await collection.findOne({ email: email.toLowerCase() });
}
/**
* Find subscription by ID
*/
static async findById(id) {
const collection = await getCollection('newsletter_subscriptions');
return await collection.findOne({ _id: new ObjectId(id) });
}
/**
* List all subscriptions
*/
static async list(options = {}) {
const collection = await getCollection('newsletter_subscriptions');
const {
limit = 100,
skip = 0,
active = true,
verified = null,
source = null
} = options;
const filter = {};
if (active !== null) filter.active = active;
if (verified !== null) filter.verified = verified;
if (source) filter.source = source;
const subscriptions = await collection
.find(filter)
.sort({ created_at: -1 })
.skip(skip)
.limit(limit)
.toArray();
return subscriptions;
}
/**
* Count subscriptions
*/
static async count(filter = {}) {
const collection = await getCollection('newsletter_subscriptions');
return await collection.countDocuments(filter);
}
/**
* Update subscription preferences
*/
static async updatePreferences(email, preferences) {
const collection = await getCollection('newsletter_subscriptions');
const result = await collection.updateOne(
{ email: email.toLowerCase(), active: true },
{
$set: {
interests: preferences.interests || [],
name: preferences.name || null,
updated_at: new Date()
}
}
);
return result.modifiedCount > 0;
}
/**
* Get subscription statistics
*/
static async getStats() {
const collection = await getCollection('newsletter_subscriptions');
const [
totalSubscribers,
activeSubscribers,
verifiedSubscribers,
recentSubscribers
] = await Promise.all([
collection.countDocuments({}),
collection.countDocuments({ active: true }),
collection.countDocuments({ active: true, verified: true }),
collection.countDocuments({
active: true,
created_at: { $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) }
})
]);
// Get source breakdown
const sourceBreakdown = await collection.aggregate([
{ $match: { active: true } },
{ $group: { _id: '$source', count: { $sum: 1 } } }
]).toArray();
return {
total: totalSubscribers,
active: activeSubscribers,
verified: verifiedSubscribers,
recent_30_days: recentSubscribers,
by_source: sourceBreakdown.reduce((acc, item) => {
acc[item._id || 'unknown'] = item.count;
return acc;
}, {})
};
}
/**
* Delete subscription (admin only)
*/
static async delete(id) {
const collection = await getCollection('newsletter_subscriptions');
const result = await collection.deleteOne({ _id: new ObjectId(id) });
return result.deletedCount > 0;
}
/**
* Generate verification token
*/
static _generateToken() {
return require('crypto').randomBytes(32).toString('hex');
}
}
module.exports = NewsletterSubscription;