Fix merge conflicts with main

This commit is contained in:
r2xrzh9q2z-lab
2026-02-04 09:21:15 +07:00
51 changed files with 6473 additions and 2727 deletions

206
models/appointment.js Normal file
View File

@@ -0,0 +1,206 @@
const mongoose = require("mongoose");
// Clear cache
if (mongoose.models.Appointment) {
delete mongoose.models.Appointment;
}
if (mongoose.connection.models.Appointment) {
delete mongoose.connection.models.Appointment;
}
// Schema cho hero section
const heroSchema = new mongoose.Schema(
{
title: {
type: String,
trim: true,
default: "Make Appointment",
},
backgroundImage: {
type: String,
trim: true,
default: "",
},
subtitle: {
type: String,
trim: true,
default: "",
},
heading: {
type: String,
trim: true,
default: "",
},
description: {
type: String,
trim: true,
default: "",
},
},
{ _id: false }
);
// Schema cho form field
const formFieldSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true,
},
label: {
type: String,
trim: true,
default: "",
},
type: {
type: String,
required: true,
trim: true,
enum: ["text", "email", "tel", "textarea", "date", "select"],
},
placeholder: {
type: String,
trim: true,
default: "",
},
required: {
type: Boolean,
default: false,
},
colClass: {
type: String,
trim: true,
default: "col-lg-12",
},
},
{ _id: false }
);
// Schema cho submit button
const submitButtonSchema = new mongoose.Schema(
{
text: {
type: String,
required: true,
trim: true,
default: "Request Appointment",
},
icon: {
type: String,
trim: true,
default: "fa-solid fa-arrow-right",
},
buttonClass: {
type: String,
trim: true,
default: "theme-btn",
},
},
{ _id: false }
);
// Schema cho form
const formSchema = new mongoose.Schema(
{
heading: {
type: String,
trim: true,
default: "Request Appointment",
},
fields: {
type: [formFieldSchema],
default: [],
},
submitButton: {
type: submitButtonSchema,
default: () => ({}),
},
},
{ _id: false }
);
// Main Appointment Schema
const appointmentSchema = new mongoose.Schema(
{
name: {
type: String,
default: "default",
unique: true,
},
hero: {
type: heroSchema,
default: () => ({}),
},
visaOptions: {
type: [String],
default: [],
},
form: {
type: formSchema,
default: () => ({}),
},
},
{
timestamps: true,
}
);
// Migration method to import data from JSON
appointmentSchema.statics.migrateFromJson = async function (jsonData) {
try {
// Check if default appointment exists
const existingAppointment = await this.findOne({ name: "default" });
// Process data from JSON
const processedData = {
hero: {
title: jsonData.hero?.title || "Make Appointment",
backgroundImage: jsonData.hero?.backgroundImage || "",
subtitle: jsonData.hero?.subtitle || "",
heading: jsonData.hero?.heading || "",
description: jsonData.hero?.description || "",
},
visaOptions: Array.isArray(jsonData.visaOptions) ? jsonData.visaOptions : [],
form: {
heading: jsonData.form?.heading || "Request Appointment",
fields: (jsonData.form?.fields || []).map((field) => ({
name: field.name || "",
label: field.label || "",
type: field.type || "text",
placeholder: field.placeholder || "",
required: field.required || false,
colClass: field.colClass || "col-lg-12",
})),
submitButton: {
text: jsonData.form?.submitButton?.text || "Request Appointment",
icon: jsonData.form?.submitButton?.icon || "fa-solid fa-arrow-right",
buttonClass: jsonData.form?.submitButton?.buttonClass || "theme-btn",
},
},
};
if (existingAppointment) {
// Update existing appointment
existingAppointment.hero = processedData.hero;
existingAppointment.visaOptions = processedData.visaOptions;
existingAppointment.form = processedData.form;
await existingAppointment.save();
console.log("Appointment data updated successfully");
return existingAppointment;
} else {
// Create new appointment
const newAppointment = await this.create({
name: "default",
...processedData,
});
console.log("Appointment data imported successfully");
return newAppointment;
}
} catch (error) {
console.error("Error migrating appointment data:", error);
throw error;
}
};
module.exports = mongoose.model("Appointment", appointmentSchema);

View File

@@ -0,0 +1,83 @@
const mongoose = require("mongoose");
/**
* Schema for Appointment Submissions
* Stores appointment requests from users
*/
const appointmentSubmissionSchema = new mongoose.Schema(
{
name: {
type: String,
required: [true, "Name is required"],
trim: true,
maxlength: [100, "Name cannot exceed 100 characters"],
},
email: {
type: String,
required: [true, "Email is required"],
trim: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, "Please enter a valid email"],
},
phone: {
type: String,
trim: true,
default: "",
},
address: {
type: String,
trim: true,
default: "",
},
appointmentDate: {
type: String,
trim: true,
default: "",
},
message: {
type: String,
trim: true,
default: "",
},
visaTypes: {
type: [String],
default: [],
},
status: {
type: String,
enum: ["pending", "confirmed", "completed", "cancelled"],
default: "pending",
},
notes: {
type: String,
trim: true,
default: "",
},
confirmedAt: {
type: Date,
default: null,
},
completedAt: {
type: Date,
default: null,
},
ipAddress: {
type: String,
default: "",
},
userAgent: {
type: String,
default: "",
},
},
{
timestamps: true,
}
);
// Index for faster queries
appointmentSubmissionSchema.index({ status: 1, createdAt: -1 });
appointmentSubmissionSchema.index({ email: 1 });
appointmentSubmissionSchema.index({ appointmentDate: 1 });
module.exports = mongoose.model("AppointmentSubmission", appointmentSubmissionSchema);

135
models/blog.js Normal file
View File

@@ -0,0 +1,135 @@
const mongoose = require('mongoose');
const blogSchema = new mongoose.Schema({
// Basic blog information
title: {
type: String,
required: true,
trim: true
},
slug: {
type: String,
required: true,
unique: true,
trim: true
},
excerpt: {
type: String,
required: true,
maxlength: 500
},
content: {
type: mongoose.Schema.Types.Mixed, // Có thể là string HTML hoặc JSON EditorJS
required: true
},
// Media
featuredImage: {
type: String,
default: ''
},
galleryImages: [{
type: String
}], // Mảng URL ảnh cho gallery (details-2, details-3)
// Author and publishing
author: {
type: String,
default: 'Admin'
},
publishedAt: {
type: String, // Format: "11 March 2025"
default: function() {
return new Date().toLocaleDateString('en-GB', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
}
},
// Categorization (simple strings, no references)
category: [{
type: String // ["Visa", "Travel", ...] - Một bài có thể thuộc nhiều category
}],
tags: [{
type: String // ["WorkVisa", "StudentVisa", ...]
}],
// Status and features
status: {
type: String,
enum: ['draft', 'published'],
default: 'published'
},
isFeatured: {
type: Boolean,
default: false
},
// Comments count (có thể fake trước)
commentsCount: {
type: Number,
default: 0
}
}, {
timestamps: true
});
// Indexes
blogSchema.index({ slug: 1 });
blogSchema.index({ status: 1, createdAt: -1 });
blogSchema.index({ category: 1, status: 1 });
blogSchema.index({ isFeatured: 1, status: 1 });
blogSchema.index({ tags: 1, status: 1 });
// Remove __v from JSON output
blogSchema.set('toJSON', {
transform: function(doc, ret) {
delete ret.__v;
return ret;
}
});
// Pre-save middleware
blogSchema.pre('save', function(next) {
// Auto-generate slug if not provided
if (!this.slug && this.title) {
this.slug = this.title
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim('-');
}
next();
});
// Static methods
blogSchema.statics.getPublished = function() {
return this.find({ status: 'published' }).sort({ createdAt: -1 });
};
blogSchema.statics.getFeatured = function() {
return this.find({
status: 'published',
isFeatured: true
}).sort({ createdAt: -1 });
};
blogSchema.statics.getByCategory = function(category) {
return this.find({
status: 'published',
category: { $in: [category] } // Tìm trong array categories
}).sort({ createdAt: -1 });
};
blogSchema.statics.getByTag = function(tag) {
return this.find({
status: 'published',
tags: tag
}).sort({ createdAt: -1 });
};
module.exports = mongoose.model('Blog', blogSchema);

76
models/blogCategory.js Normal file
View File

@@ -0,0 +1,76 @@
const mongoose = require('mongoose');
const blogCategorySchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
unique: true // "Permanent Residency (PR)"
},
slug: {
type: String,
required: true,
unique: true,
trim: true // "permanent-residency"
},
postCount: {
type: Number,
default: 0 // "(04)"
},
description: {
type: String,
default: ''
},
isActive: {
type: Boolean,
default: true
}
}, {
timestamps: true
});
// Indexes
blogCategorySchema.index({ slug: 1 });
blogCategorySchema.index({ isActive: 1, name: 1 });
// Remove __v from JSON output
blogCategorySchema.set('toJSON', {
transform: function(doc, ret) {
delete ret.__v;
return ret;
}
});
// Pre-save middleware
blogCategorySchema.pre('save', function(next) {
// Auto-generate slug if not provided
if (!this.slug && this.name) {
this.slug = this.name
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim('-');
}
next();
});
// Static methods
blogCategorySchema.statics.getActive = function() {
return this.find({ isActive: true }).sort({ name: 1 });
};
// Method to update post count
blogCategorySchema.methods.updatePostCount = async function() {
const Blog = require('./blog');
const count = await Blog.countDocuments({
category: { $in: [this.name] }, // Tìm trong array categories
status: 'published'
});
this.postCount = count;
await this.save();
return count;
};
module.exports = mongoose.model('BlogCategory', blogCategorySchema);

84
models/blogComment.js Normal file
View File

@@ -0,0 +1,84 @@
const mongoose = require('mongoose');
const blogCommentSchema = new mongoose.Schema({
postId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Blog',
required: true
},
authorName: {
type: String,
required: true,
trim: true // "Frank Flores"
},
authorAvatar: {
type: String,
default: '' // "/assets/img/inner-page/news-details/comment-1.png"
},
content: {
type: String,
required: true,
trim: true
},
parentId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'BlogComment',
default: null // Cho threaded comments (reply)
},
status: {
type: String,
enum: ['pending', 'approved', 'rejected'],
default: 'pending'
}
}, {
timestamps: true // Vẫn giữ timestamps cho admin quản lý
});
// Indexes
blogCommentSchema.index({ postId: 1, status: 1, createdAt: -1 });
blogCommentSchema.index({ parentId: 1 });
blogCommentSchema.index({ status: 1 });
// Remove __v from JSON output
blogCommentSchema.set('toJSON', {
transform: function(doc, ret) {
delete ret.__v;
return ret;
}
});
// Static methods
blogCommentSchema.statics.getApprovedByPost = function(postId) {
return this.find({
postId: postId,
status: 'approved',
parentId: null // Chỉ lấy comments gốc, không lấy replies
}).sort({ createdAt: -1 });
};
blogCommentSchema.statics.getReplies = function(parentId) {
return this.find({
parentId: parentId,
status: 'approved'
}).sort({ createdAt: 1 }); // Replies sắp xếp theo thời gian tăng dần
};
blogCommentSchema.statics.getByStatus = function(status) {
return this.find({ status: status })
.populate('postId', 'title slug')
.sort({ createdAt: -1 });
};
// Method to approve comment
blogCommentSchema.methods.approve = function() {
this.status = 'approved';
return this.save();
};
// Method to reject comment
blogCommentSchema.methods.reject = function() {
this.status = 'rejected';
return this.save();
};
module.exports = mongoose.model('BlogComment', blogCommentSchema);

78
models/blogTag.js Normal file
View File

@@ -0,0 +1,78 @@
const mongoose = require('mongoose');
const blogTagSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
unique: true // "WorkVisa"
},
slug: {
type: String,
required: true,
unique: true,
trim: true // "work-visa"
},
postCount: {
type: Number,
default: 0
},
isActive: {
type: Boolean,
default: true
}
}, {
timestamps: true
});
// Indexes
blogTagSchema.index({ slug: 1 });
blogTagSchema.index({ isActive: 1, name: 1 });
// Remove __v from JSON output
blogTagSchema.set('toJSON', {
transform: function(doc, ret) {
delete ret.__v;
return ret;
}
});
// Pre-save middleware
blogTagSchema.pre('save', function(next) {
// Auto-generate slug if not provided
if (!this.slug && this.name) {
this.slug = this.name
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim('-');
}
next();
});
// Static methods
blogTagSchema.statics.getActive = function() {
return this.find({ isActive: true }).sort({ name: 1 });
};
blogTagSchema.statics.getPopular = function(limit = 10) {
return this.find({ isActive: true })
.sort({ postCount: -1, name: 1 })
.limit(limit);
};
// Method to update post count
blogTagSchema.methods.updatePostCount = async function() {
const Blog = require('./blog');
const count = await Blog.countDocuments({
tags: { $in: [this.name] }, // Tìm trong array tags
status: 'published'
});
this.postCount = count;
await this.save();
return count;
};
module.exports = mongoose.model('BlogTag', blogTagSchema);

View File

@@ -145,6 +145,11 @@ const mapSchema = new mongoose.Schema(
trim: true,
default: "",
},
embedUrl: {
type: String,
trim: true,
default: "",
},
tileLayer: {
type: tileLayerSchema,
required: true,
@@ -161,11 +166,16 @@ const formFieldSchema = new mongoose.Schema(
required: true,
trim: true,
},
label: {
type: String,
trim: true,
default: "",
},
type: {
type: String,
required: true,
trim: true,
enum: ["text", "email", "tel", "textarea", "programme"],
enum: ["text", "email", "tel", "textarea", "programme", "date"],
},
placeholder: {
type: String,
@@ -176,6 +186,11 @@ const formFieldSchema = new mongoose.Schema(
type: Boolean,
default: false,
},
colClass: {
type: String,
trim: true,
default: "col-lg-12",
},
programmeName: {
type: String,
trim: true,
@@ -193,6 +208,16 @@ const submitButtonSchema = new mongoose.Schema(
required: true,
trim: true,
},
icon: {
type: String,
trim: true,
default: "fa-solid fa-arrow-right",
},
buttonClass: {
type: String,
trim: true,
default: "theme-btn style-2",
},
},
{ _id: false }
);
@@ -210,6 +235,11 @@ const formSchema = new mongoose.Schema(
trim: true,
default: "",
},
description: {
type: String,
trim: true,
default: "",
},
fields: {
type: [formFieldSchema],
default: [],
@@ -335,6 +365,7 @@ contactSchema.statics.migrateFromJson = async function (jsonData) {
zoom: jsonData.map?.zoom || 15,
location: jsonData.map?.location || "",
markerTitle: jsonData.map?.markerTitle || "",
embedUrl: jsonData.map?.embedUrl || "",
tileLayer: {
url:
jsonData.map?.tileLayer?.url ||
@@ -347,15 +378,20 @@ contactSchema.statics.migrateFromJson = async function (jsonData) {
form: {
sectionLabel: jsonData.form?.sectionLabel || "",
heading: jsonData.form?.heading || "",
description: jsonData.form?.description || "",
fields: (jsonData.form?.fields || []).map((field) => ({
name: field.name || "",
label: field.label || "",
type: field.type || "text",
placeholder: field.placeholder || "",
required: field.required || false,
colClass: field.colClass || "col-lg-12",
programmeName: field.programmeName || "",
})),
submitButton: {
text: jsonData.form?.submitButton?.text || "Send Message",
icon: jsonData.form?.submitButton?.icon || "fa-solid fa-arrow-right",
buttonClass: jsonData.form?.submitButton?.buttonClass || "theme-btn style-2",
},
},
};

View File

@@ -0,0 +1,74 @@
const mongoose = require("mongoose");
/**
* Schema for Contact Form Submissions
* Stores user inquiries from the contact form
*/
const contactSubmissionSchema = new mongoose.Schema(
{
name: {
type: String,
required: [true, "Name is required"],
trim: true,
maxlength: [100, "Name cannot exceed 100 characters"],
},
email: {
type: String,
required: [true, "Email is required"],
trim: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, "Please enter a valid email"],
},
phone: {
type: String,
trim: true,
default: "",
},
address: {
type: String,
trim: true,
default: "",
},
date: {
type: String,
trim: true,
default: "",
},
message: {
type: String,
trim: true,
default: "",
},
status: {
type: String,
enum: ["pending", "read", "replied", "archived"],
default: "pending",
},
notes: {
type: String,
trim: true,
default: "",
},
repliedAt: {
type: Date,
default: null,
},
ipAddress: {
type: String,
default: "",
},
userAgent: {
type: String,
default: "",
},
},
{
timestamps: true,
}
);
// Index for faster queries
contactSubmissionSchema.index({ status: 1, createdAt: -1 });
contactSubmissionSchema.index({ email: 1 });
module.exports = mongoose.model("ContactSubmission", contactSubmissionSchema);

328
models/pricing.js Normal file
View File

@@ -0,0 +1,328 @@
const mongoose = require("mongoose");
// Clear cache
if (mongoose.models.Pricing) {
delete mongoose.models.Pricing;
}
if (mongoose.connection.models.Pricing) {
delete mongoose.connection.models.Pricing;
}
// Schema for breadcrumb item
const breadcrumbItemSchema = new mongoose.Schema(
{
text: {
type: String,
trim: true,
default: "",
},
link: {
type: String,
trim: true,
default: "",
},
},
{ _id: false }
);
// Schema for hero section
const heroSchema = new mongoose.Schema(
{
title: {
type: String,
trim: true,
default: "Pricing Plan",
},
backgroundImage: {
type: String,
trim: true,
default: "/assets/img/inner-page/breadcrumb.jpg",
},
shapeImage: {
type: String,
trim: true,
default: "/assets/img/inner-page/shape.png",
},
breadcrumb: {
type: [breadcrumbItemSchema],
default: [],
},
},
{ _id: false }
);
// Schema for pricing section header
const pricingSectionSchema = new mongoose.Schema(
{
subtitle: {
type: String,
trim: true,
default: "pricing plan",
},
heading: {
type: String,
trim: true,
default: "Flexible Plans to Suit Every Traveler",
},
description: {
type: String,
trim: true,
default: "",
},
},
{ _id: false }
);
// Schema for individual plan
const planSchema = new mongoose.Schema(
{
name: {
type: String,
trim: true,
required: true,
},
price: {
type: String,
trim: true,
default: "0",
},
period: {
type: String,
trim: true,
default: "mo",
},
currency: {
type: String,
trim: true,
default: "$",
},
buttonText: {
type: String,
trim: true,
default: "Get Started Today",
},
buttonLink: {
type: String,
trim: true,
default: "/pricing",
},
buttonIcon: {
type: String,
trim: true,
default: "fa-solid fa-arrow-right",
},
style: {
type: String,
trim: true,
enum: ["default", "style-2"],
default: "default",
},
features: {
type: [String],
default: [],
},
},
{ _id: false }
);
// Schema for plans container
const plansSchema = new mongoose.Schema(
{
monthly: {
type: [planSchema],
default: [],
},
yearly: {
type: [planSchema],
default: [],
},
},
{ _id: false }
);
// Schema for testimonial item
const testimonialItemSchema = new mongoose.Schema(
{
name: {
type: String,
trim: true,
default: "",
},
role: {
type: String,
trim: true,
default: "",
},
rating: {
type: Number,
min: 1,
max: 5,
default: 5,
},
content: {
type: String,
trim: true,
default: "",
},
},
{ _id: false }
);
// Schema for testimonials section
const testimonialsSchema = new mongoose.Schema(
{
subtitle: {
type: String,
trim: true,
default: "What Our Clients Say",
},
heading: {
type: String,
trim: true,
default: "Immigration Success Stories",
},
buttonText: {
type: String,
trim: true,
default: "View All Review",
},
buttonLink: {
type: String,
trim: true,
default: "/contact",
},
buttonIcon: {
type: String,
trim: true,
default: "fa-solid fa-arrow-right",
},
image: {
type: String,
trim: true,
default: "",
},
items: {
type: [testimonialItemSchema],
default: [],
},
},
{ _id: false }
);
// Main Pricing Schema
const pricingSchema = new mongoose.Schema(
{
name: {
type: String,
default: "default",
unique: true,
},
hero: {
type: heroSchema,
default: () => ({}),
},
pricingSection: {
type: pricingSectionSchema,
default: () => ({}),
},
plans: {
type: plansSchema,
default: () => ({}),
},
testimonials: {
type: testimonialsSchema,
default: () => ({}),
},
},
{
timestamps: true,
}
);
// Migration method to import data from JSON
pricingSchema.statics.migrateFromJson = async function (jsonData) {
try {
// Check if default pricing exists
const existingPricing = await this.findOne({ name: "default" });
// Process data from JSON
const processedData = {
hero: {
title: jsonData.hero?.title || "Pricing Plan",
backgroundImage: jsonData.hero?.backgroundImage || "/assets/img/inner-page/breadcrumb.jpg",
shapeImage: jsonData.hero?.shapeImage || "/assets/img/inner-page/shape.png",
breadcrumb: (jsonData.hero?.breadcrumb || []).map((item) => ({
text: item.text || "",
link: item.link || "",
})),
},
pricingSection: {
subtitle: jsonData.pricingSection?.subtitle || "pricing plan",
heading: jsonData.pricingSection?.heading || "Flexible Plans to Suit Every Traveler",
description: jsonData.pricingSection?.description || "",
},
plans: {
monthly: (jsonData.plans?.monthly || []).map((plan) => ({
name: plan.name || "",
price: plan.price || "0",
period: plan.period || "mo",
currency: plan.currency || "$",
buttonText: plan.buttonText || "Get Started Today",
buttonLink: plan.buttonLink || "/pricing",
buttonIcon: plan.buttonIcon || "fa-solid fa-arrow-right",
style: plan.style || "default",
features: plan.features || [],
})),
yearly: (jsonData.plans?.yearly || []).map((plan) => ({
name: plan.name || "",
price: plan.price || "0",
period: plan.period || "mo",
currency: plan.currency || "$",
buttonText: plan.buttonText || "Get Started Today",
buttonLink: plan.buttonLink || "/pricing",
buttonIcon: plan.buttonIcon || "fa-solid fa-arrow-right",
style: plan.style || "default",
features: plan.features || [],
})),
},
testimonials: {
subtitle: jsonData.testimonials?.subtitle || "What Our Clients Say",
heading: jsonData.testimonials?.heading || "Immigration Success Stories",
buttonText: jsonData.testimonials?.buttonText || "View All Review",
buttonLink: jsonData.testimonials?.buttonLink || "/contact",
buttonIcon: jsonData.testimonials?.buttonIcon || "fa-solid fa-arrow-right",
image: jsonData.testimonials?.image || "",
items: (jsonData.testimonials?.items || []).map((item) => ({
name: item.name || "",
role: item.role || "",
rating: item.rating || 5,
content: item.content || "",
})),
},
};
if (existingPricing) {
// Update existing pricing
existingPricing.hero = processedData.hero;
existingPricing.pricingSection = processedData.pricingSection;
existingPricing.plans = processedData.plans;
existingPricing.testimonials = processedData.testimonials;
await existingPricing.save();
console.log("Pricing data updated successfully");
return existingPricing;
} else {
// Create new pricing
const newPricing = await this.create({
name: "default",
...processedData,
});
console.log("Pricing data imported successfully");
return newPricing;
}
} catch (error) {
console.error("Error migrating pricing data:", error);
throw error;
}
};
module.exports = mongoose.model("Pricing", pricingSchema);

79
models/recentPost.js Normal file
View File

@@ -0,0 +1,79 @@
const mongoose = require('mongoose');
// Recent Post model - có thể là view hoặc collection riêng để optimize performance
const recentPostSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true
},
slug: {
type: String,
required: true,
trim: true
},
thumbnail: {
type: String,
default: '' // Ảnh nhỏ ở sidebar
},
publishedAt: {
type: String, // "March 26, 2025"
required: true
},
// Reference to original blog post
originalPostId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Blog',
required: true
}
}, {
timestamps: true
});
// Indexes
recentPostSchema.index({ createdAt: -1 });
recentPostSchema.index({ originalPostId: 1 });
// Remove __v from JSON output
recentPostSchema.set('toJSON', {
transform: function(doc, ret) {
delete ret.__v;
return ret;
}
});
// Static method to sync with Blog posts
recentPostSchema.statics.syncFromBlogs = async function(limit = 5) {
const Blog = require('./blog');
// Get recent published blogs
const recentBlogs = await Blog.find({ status: 'published' })
.sort({ createdAt: -1 })
.limit(limit)
.select('title slug featuredImage publishedAt');
// Clear existing recent posts
await this.deleteMany({});
// Create new recent posts
const recentPosts = recentBlogs.map(blog => ({
title: blog.title,
slug: blog.slug,
thumbnail: blog.featuredImage,
publishedAt: blog.publishedAt,
originalPostId: blog._id
}));
if (recentPosts.length > 0) {
await this.insertMany(recentPosts);
}
return recentPosts;
};
// Static method to get recent posts
recentPostSchema.statics.getRecent = function(limit = 5) {
return this.find({}).sort({ createdAt: -1 }).limit(limit);
};
module.exports = mongoose.model('RecentPost', recentPostSchema);