feat:Add blog management page and enhance admin layout

This commit is contained in:
Wini_Fy
2026-02-03 17:04:28 +07:00
parent bb539ef213
commit 538317eade
14 changed files with 3096 additions and 277 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,6 @@
# dependencies # dependencies
node_modules/ node_modules/
/public
# environment # environment
.env .env
@@ -21,3 +22,4 @@ pids
#cursor #cursor
.cursor .cursor
package-lock.json

View File

@@ -1,4 +1,5 @@
const BlogCategory = require('../models/blogCategory'); const BlogCategory = require('../models/blogCategory');
const slugify = require('slugify');
// -------------------- Admin Controllers -------------------- // -------------------- Admin Controllers --------------------
@@ -49,12 +50,11 @@ exports.store = async (req, res) => {
} = req.body; } = req.body;
// Generate slug // Generate slug
const slug = name const slug = slugify(name, {
.toLowerCase() lower: true,
.replace(/[^a-z0-9\s-]/g, '') strict: true,
.replace(/\s+/g, '-') locale: 'vi'
.replace(/-+/g, '-') });
.trim('-');
// Check if slug exists // Check if slug exists
const existingCategory = await BlogCategory.findOne({ slug }); const existingCategory = await BlogCategory.findOne({ slug });
@@ -130,12 +130,11 @@ exports.update = async (req, res) => {
category.isActive = isActive === 'on'; category.isActive = isActive === 'on';
// Generate new slug if name changed // Generate new slug if name changed
const newSlug = name const newSlug = slugify(name, {
.toLowerCase() lower: true,
.replace(/[^a-z0-9\s-]/g, '') strict: true,
.replace(/\s+/g, '-') locale: 'vi'
.replace(/-+/g, '-') });
.trim('-');
if (newSlug !== category.slug) { if (newSlug !== category.slug) {
const existingCategory = await BlogCategory.findOne({ const existingCategory = await BlogCategory.findOne({
@@ -166,6 +165,14 @@ exports.destroy = async (req, res) => {
const category = await BlogCategory.findById(req.params.id); const category = await BlogCategory.findById(req.params.id);
if (!category) { if (!category) {
// Check if it's an AJAX request
const isAjax = req.xhr || req.headers['accept']?.includes('application/json') || req.headers['content-type']?.includes('application/json');
if (isAjax) {
return res.status(404).json({
success: false,
message: 'Category not found'
});
}
req.flash('error_msg', 'Category not found'); req.flash('error_msg', 'Category not found');
return res.redirect('/admin/blog/categories'); return res.redirect('/admin/blog/categories');
} }
@@ -175,16 +182,44 @@ exports.destroy = async (req, res) => {
const postCount = await Blog.countDocuments({ category: { $in: [category.name] } }); const postCount = await Blog.countDocuments({ category: { $in: [category.name] } });
if (postCount > 0) { if (postCount > 0) {
// Check if it's an AJAX request
const isAjax = req.xhr || req.headers['accept']?.includes('application/json') || req.headers['content-type']?.includes('application/json');
if (isAjax) {
return res.status(400).json({
success: false,
message: 'Cannot delete category that has blog posts'
});
}
req.flash('error_msg', 'Cannot delete category that has blog posts'); req.flash('error_msg', 'Cannot delete category that has blog posts');
return res.redirect('/admin/blog/categories'); return res.redirect('/admin/blog/categories');
} }
await BlogCategory.findByIdAndDelete(req.params.id); await BlogCategory.findByIdAndDelete(req.params.id);
// Check if it's an AJAX request
const isAjax = req.xhr || req.headers['accept']?.includes('application/json') || req.headers['content-type']?.includes('application/json');
if (isAjax) {
return res.json({
success: true,
message: 'Category deleted successfully'
});
}
req.flash('success_msg', 'Category deleted successfully'); req.flash('success_msg', 'Category deleted successfully');
res.redirect('/admin/blog/categories'); res.redirect('/admin/blog/categories');
} catch (err) { } catch (err) {
console.error('Category delete error:', err); console.error('Category delete error:', err);
// Check if it's an AJAX request
const isAjax = req.xhr || req.headers['accept']?.includes('application/json') || req.headers['content-type']?.includes('application/json');
if (isAjax) {
return res.status(500).json({
success: false,
message: 'Error deleting category',
error: err.message || 'Error deleting category'
});
}
req.flash('error_msg', 'Error deleting category'); req.flash('error_msg', 'Error deleting category');
res.redirect('/admin/blog/categories'); res.redirect('/admin/blog/categories');
} }
@@ -202,10 +237,18 @@ exports.api = async (req, res) => {
await category.updatePostCount(); await category.updatePostCount();
} }
res.json(categories); res.json({
success: true,
message: 'Categories fetched successfully',
data: categories
});
} catch (err) { } catch (err) {
console.error('Categories API error:', err); console.error('Categories API error:', err);
res.status(500).json({ error: 'Error loading categories' }); res.status(500).json({
success: false,
message: 'Error loading categories',
error: err.message || 'Error loading categories'
});
} }
}; };
@@ -218,13 +261,81 @@ exports.apiShow = async (req, res) => {
}).lean(); }).lean();
if (!category) { if (!category) {
return res.status(404).json({ error: 'Category not found' }); return res.status(404).json({
success: false,
message: 'Category not found'
});
} }
res.json(category); res.json({
success: true,
message: 'Category fetched successfully',
data: category
});
} catch (err) { } catch (err) {
console.error('Category show API error:', err); console.error('Category show API error:', err);
res.status(500).json({ error: 'Error loading category' }); res.status(500).json({
success: false,
message: 'Error loading category',
error: err.message || 'Error loading category'
});
}
};
// Quick create category (for inline creation in blog form)
exports.quickCreate = async (req, res) => {
try {
const { name, description } = req.body;
if (!name || !name.trim()) {
return res.status(400).json({
success: false,
message: 'Category name is required'
});
}
const categoryName = name.trim();
// Generate slug
const slug = slugify(categoryName, {
lower: true,
strict: true,
locale: 'vi'
});
// Check if category already exists
let category = await BlogCategory.findOne({ slug });
if (category) {
return res.json({
success: true,
message: 'Category already exists',
data: category.toObject()
});
}
// Create new category
category = new BlogCategory({
name: categoryName,
slug,
description: description || '',
isActive: true
});
await category.save();
res.json({
success: true,
message: 'Category created successfully',
data: category.toObject()
});
} catch (err) {
console.error('Quick create category error:', err);
res.status(500).json({
success: false,
message: 'Error creating category',
error: err.message || 'Error creating category'
});
} }
}; };

View File

@@ -4,17 +4,17 @@ const BlogTag = require('../models/blogTag');
const BlogComment = require('../models/blogComment'); const BlogComment = require('../models/blogComment');
const RecentPost = require('../models/recentPost'); const RecentPost = require('../models/recentPost');
const { addBaseUrlToImages } = require('../utils/imageHelper'); const { addBaseUrlToImages } = require('../utils/imageHelper');
const slugify = require('slugify');
// -------------------- Helper Functions -------------------- // -------------------- Helper Functions --------------------
// Generate slug from title // Generate slug from title
const generateSlug = (title) => { const generateSlug = (title) => {
return title return slugify(title, {
.toLowerCase() lower: true,
.replace(/[^a-z0-9\s-]/g, '') strict: true,
.replace(/\s+/g, '-') locale: 'vi'
.replace(/-+/g, '-') });
.trim('-');
}; };
// Update category post counts // Update category post counts
@@ -69,12 +69,15 @@ exports.index = async (req, res) => {
// Get categories for filter // Get categories for filter
const categories = await BlogCategory.getActive(); const categories = await BlogCategory.getActive();
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
res.render('admin/blog/index', { res.render('admin/blog/index', {
layout: 'layouts/main', layout: 'layouts/main',
title: 'Blog Management', title: 'Blog Management',
blogs, blogs,
categories, categories,
frontendUrl,
pagination: { pagination: {
current: page, current: page,
total: totalPages, total: totalPages,
@@ -98,13 +101,16 @@ exports.create = async (req, res) => {
const categories = await BlogCategory.getActive(); const categories = await BlogCategory.getActive();
const tags = await BlogTag.getActive(); const tags = await BlogTag.getActive();
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
res.render('admin/blog/create', { res.render('admin/blog/create', {
layout: 'layouts/main', layout: 'layouts/main',
title: 'Create New Blog Post', title: 'Create New Blog Post',
categories, categories,
tags, tags,
currentPath: req.path, currentPath: req.path,
user: req.session.user user: req.session.user,
frontendUrl
}); });
} catch (err) { } catch (err) {
console.error('Blog create form error:', err); console.error('Blog create form error:', err);
@@ -125,7 +131,9 @@ exports.store = async (req, res) => {
status, status,
isFeatured, isFeatured,
author, author,
galleryImages galleryImages,
quote,
contentAfterQuote
} = req.body; } = req.body;
// Generate slug // Generate slug
@@ -149,12 +157,14 @@ exports.store = async (req, res) => {
status: status || 'published', status: status || 'published',
isFeatured: isFeatured === 'on', isFeatured: isFeatured === 'on',
author: author || 'Admin', author: author || 'Admin',
galleryImages: galleryImages ? (Array.isArray(galleryImages) ? galleryImages : [galleryImages]) : [] galleryImages: galleryImages ? (Array.isArray(galleryImages) ? galleryImages : [galleryImages]) : [],
quote: quote || '',
contentAfterQuote: contentAfterQuote || ''
}; };
// Handle featured image // Handle featured image - using featuredImageUrl from form (uploaded via AJAX)
if (req.file) { if (req.body.featuredImageUrl) {
blogData.featuredImage = `/uploads/blog/${req.file.filename}`; blogData.featuredImage = req.body.featuredImageUrl;
} }
// Create blog // Create blog
@@ -188,6 +198,8 @@ exports.edit = async (req, res) => {
const categories = await BlogCategory.getActive(); const categories = await BlogCategory.getActive();
const tags = await BlogTag.getActive(); const tags = await BlogTag.getActive();
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
res.render('admin/blog/edit', { res.render('admin/blog/edit', {
layout: 'layouts/main', layout: 'layouts/main',
title: 'Edit Blog Post', title: 'Edit Blog Post',
@@ -195,7 +207,8 @@ exports.edit = async (req, res) => {
categories, categories,
tags, tags,
currentPath: req.path, currentPath: req.path,
user: req.session.user user: req.session.user,
frontendUrl
}); });
} catch (err) { } catch (err) {
console.error('Blog edit form error:', err); console.error('Blog edit form error:', err);
@@ -223,7 +236,9 @@ exports.update = async (req, res) => {
status, status,
isFeatured, isFeatured,
author, author,
galleryImages galleryImages,
quote,
contentAfterQuote
} = req.body; } = req.body;
// Update blog data // Update blog data
@@ -236,10 +251,12 @@ exports.update = async (req, res) => {
blog.isFeatured = isFeatured === 'on'; blog.isFeatured = isFeatured === 'on';
blog.author = author || 'Admin'; blog.author = author || 'Admin';
blog.galleryImages = galleryImages ? (Array.isArray(galleryImages) ? galleryImages : [galleryImages]) : []; blog.galleryImages = galleryImages ? (Array.isArray(galleryImages) ? galleryImages : [galleryImages]) : [];
blog.quote = quote || '';
blog.contentAfterQuote = contentAfterQuote || '';
// Handle featured image // Handle featured image - using featuredImageUrl from form (uploaded via AJAX)
if (req.file) { if (req.body.featuredImageUrl) {
blog.featuredImage = `/uploads/blog/${req.file.filename}`; blog.featuredImage = req.body.featuredImageUrl;
} }
// Generate new slug if title changed // Generate new slug if title changed
@@ -373,11 +390,30 @@ exports.apiShow = async (req, res) => {
}); });
} }
// Get comments for this post // Get comments for this post (parent comments only)
const comments = await BlogComment.getApprovedByPost(blog._id); const parentComments = await BlogComment.getApprovedByPost(blog._id);
// Get replies for each parent comment
const commentsWithReplies = await Promise.all(
parentComments.map(async (parentComment) => {
const replies = await BlogComment.getReplies(parentComment._id);
return {
...parentComment.toObject(),
replies: replies.map(reply => reply.toObject())
};
})
);
// Flatten comments array (parent + replies)
const allComments = commentsWithReplies.flatMap(comment => [
comment,
...comment.replies
]);
// Add comments to blog // Add comments to blog
blog.comments = comments; blog.comments = allComments;
// Keep commentsCount in sync for frontend
blog.commentsCount = allComments.length;
// Add base URL to images // Add base URL to images
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`; const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
@@ -398,6 +434,72 @@ exports.apiShow = async (req, res) => {
} }
}; };
// Create a comment (no moderation for now: default approved)
exports.apiCreateComment = async (req, res) => {
try {
const { authorName, content, parentId } = req.body || {};
if (!authorName || !String(authorName).trim()) {
return res.status(400).json({
success: false,
message: "authorName is required",
});
}
if (!content || !String(content).trim()) {
return res.status(400).json({
success: false,
message: "content is required",
});
}
const blog = await Blog.findOne({ slug: req.params.slug, status: "published" }).lean();
if (!blog) {
return res.status(404).json({
success: false,
message: "Blog post not found",
});
}
// If replying, ensure parent exists and belongs to same post
let parentObjectId = null;
if (parentId) {
const parent = await BlogComment.findOne({ _id: parentId, postId: blog._id }).lean();
if (!parent) {
return res.status(400).json({
success: false,
message: "Invalid parentId",
});
}
parentObjectId = parentId;
}
const newComment = await BlogComment.create({
postId: blog._id,
authorName: String(authorName).trim(),
content: String(content).trim(),
parentId: parentObjectId,
status: "approved",
});
// Keep counter roughly correct (also counts replies)
await Blog.updateOne({ _id: blog._id }, { $inc: { commentsCount: 1 } });
return res.json({
success: true,
message: "Comment created successfully",
data: newComment.toJSON(),
});
} catch (err) {
console.error("Create comment API error:", err);
return res.status(500).json({
success: false,
message: "Error creating comment",
error: err.message || "Error creating comment",
});
}
};
// Get featured blogs // Get featured blogs
exports.apiFeatured = async (req, res) => { exports.apiFeatured = async (req, res) => {
try { try {
@@ -408,7 +510,7 @@ exports.apiFeatured = async (req, res) => {
.lean(); .lean();
// Add base URL to images // Add base URL to images
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`; const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('port')}`;
const processedBlogs = blogs.map(blog => addBaseUrlToImages(blog, baseUrl)); const processedBlogs = blogs.map(blog => addBaseUrlToImages(blog, baseUrl));
res.json({ res.json({

View File

@@ -1,4 +1,5 @@
const BlogTag = require('../models/blogTag'); const BlogTag = require('../models/blogTag');
const slugify = require('slugify');
// -------------------- Admin Controllers -------------------- // -------------------- Admin Controllers --------------------
@@ -48,12 +49,11 @@ exports.store = async (req, res) => {
} = req.body; } = req.body;
// Generate slug // Generate slug
const slug = name const slug = slugify(name, {
.toLowerCase() lower: true,
.replace(/[^a-z0-9\s-]/g, '') strict: true,
.replace(/\s+/g, '-') locale: 'vi'
.replace(/-+/g, '-') });
.trim('-');
// Check if slug exists // Check if slug exists
const existingTag = await BlogTag.findOne({ slug }); const existingTag = await BlogTag.findOne({ slug });
@@ -126,12 +126,11 @@ exports.update = async (req, res) => {
tag.isActive = isActive === 'on'; tag.isActive = isActive === 'on';
// Generate new slug if name changed // Generate new slug if name changed
const newSlug = name const newSlug = slugify(name, {
.toLowerCase() lower: true,
.replace(/[^a-z0-9\s-]/g, '') strict: true,
.replace(/\s+/g, '-') locale: 'vi'
.replace(/-+/g, '-') });
.trim('-');
if (newSlug !== tag.slug) { if (newSlug !== tag.slug) {
const existingTag = await BlogTag.findOne({ const existingTag = await BlogTag.findOne({
@@ -162,6 +161,14 @@ exports.destroy = async (req, res) => {
const tag = await BlogTag.findById(req.params.id); const tag = await BlogTag.findById(req.params.id);
if (!tag) { if (!tag) {
// Check if it's an AJAX request
const isAjax = req.xhr || req.headers['accept']?.includes('application/json') || req.headers['content-type']?.includes('application/json');
if (isAjax) {
return res.status(404).json({
success: false,
message: 'Tag not found'
});
}
req.flash('error_msg', 'Tag not found'); req.flash('error_msg', 'Tag not found');
return res.redirect('/admin/blog/tags'); return res.redirect('/admin/blog/tags');
} }
@@ -171,16 +178,44 @@ exports.destroy = async (req, res) => {
const postCount = await Blog.countDocuments({ tags: { $in: [tag.name] } }); const postCount = await Blog.countDocuments({ tags: { $in: [tag.name] } });
if (postCount > 0) { if (postCount > 0) {
// Check if it's an AJAX request
const isAjax = req.xhr || req.headers['accept']?.includes('application/json') || req.headers['content-type']?.includes('application/json');
if (isAjax) {
return res.status(400).json({
success: false,
message: 'Cannot delete tag that is used in blog posts'
});
}
req.flash('error_msg', 'Cannot delete tag that is used in blog posts'); req.flash('error_msg', 'Cannot delete tag that is used in blog posts');
return res.redirect('/admin/blog/tags'); return res.redirect('/admin/blog/tags');
} }
await BlogTag.findByIdAndDelete(req.params.id); await BlogTag.findByIdAndDelete(req.params.id);
// Check if it's an AJAX request
const isAjax = req.xhr || req.headers['accept']?.includes('application/json') || req.headers['content-type']?.includes('application/json');
if (isAjax) {
return res.json({
success: true,
message: 'Tag deleted successfully'
});
}
req.flash('success_msg', 'Tag deleted successfully'); req.flash('success_msg', 'Tag deleted successfully');
res.redirect('/admin/blog/tags'); res.redirect('/admin/blog/tags');
} catch (err) { } catch (err) {
console.error('Tag delete error:', err); console.error('Tag delete error:', err);
// Check if it's an AJAX request
const isAjax = req.xhr || req.headers['accept']?.includes('application/json') || req.headers['content-type']?.includes('application/json');
if (isAjax) {
return res.status(500).json({
success: false,
message: 'Error deleting tag',
error: err.message || 'Error deleting tag'
});
}
req.flash('error_msg', 'Error deleting tag'); req.flash('error_msg', 'Error deleting tag');
res.redirect('/admin/blog/tags'); res.redirect('/admin/blog/tags');
} }
@@ -198,10 +233,18 @@ exports.api = async (req, res) => {
await tag.updatePostCount(); await tag.updatePostCount();
} }
res.json(tags); res.json({
success: true,
message: 'Tags fetched successfully',
data: tags
});
} catch (err) { } catch (err) {
console.error('Tags API error:', err); console.error('Tags API error:', err);
res.status(500).json({ error: 'Error loading tags' }); res.status(500).json({
success: false,
message: 'Error loading tags',
error: err.message || 'Error loading tags'
});
} }
}; };
@@ -210,10 +253,19 @@ exports.apiPopular = async (req, res) => {
try { try {
const limit = parseInt(req.query.limit) || 10; const limit = parseInt(req.query.limit) || 10;
const tags = await BlogTag.getPopular(limit); const tags = await BlogTag.getPopular(limit);
res.json(tags);
res.json({
success: true,
message: 'Popular tags fetched successfully',
data: tags
});
} catch (err) { } catch (err) {
console.error('Popular tags API error:', err); console.error('Popular tags API error:', err);
res.status(500).json({ error: 'Error loading popular tags' }); res.status(500).json({
success: false,
message: 'Error loading popular tags',
error: err.message || 'Error loading popular tags'
});
} }
}; };
@@ -226,13 +278,80 @@ exports.apiShow = async (req, res) => {
}).lean(); }).lean();
if (!tag) { if (!tag) {
return res.status(404).json({ error: 'Tag not found' }); return res.status(404).json({
success: false,
message: 'Tag not found'
});
} }
res.json(tag); res.json({
success: true,
message: 'Tag fetched successfully',
data: tag
});
} catch (err) { } catch (err) {
console.error('Tag show API error:', err); console.error('Tag show API error:', err);
res.status(500).json({ error: 'Error loading tag' }); res.status(500).json({
success: false,
message: 'Error loading tag',
error: err.message || 'Error loading tag'
});
}
};
// Quick create tag (for inline creation in blog form)
exports.quickCreate = async (req, res) => {
try {
const { name } = req.body;
if (!name || !name.trim()) {
return res.status(400).json({
success: false,
message: 'Tag name is required'
});
}
const tagName = name.trim();
// Generate slug
const slug = slugify(tagName, {
lower: true,
strict: true,
locale: 'vi'
});
// Check if tag already exists
let tag = await BlogTag.findOne({ slug });
if (tag) {
return res.json({
success: true,
message: 'Tag already exists',
data: tag.toObject()
});
}
// Create new tag
tag = new BlogTag({
name: tagName,
slug,
isActive: true
});
await tag.save();
res.json({
success: true,
message: 'Tag created successfully',
data: tag.toObject()
});
} catch (err) {
console.error('Quick create tag error:', err);
res.status(500).json({
success: false,
message: 'Error creating tag',
error: err.message || 'Error creating tag'
});
} }
}; };

View File

@@ -50,43 +50,53 @@
"status": "published", "status": "published",
"publishedAt": "11 March 2025", "publishedAt": "11 March 2025",
"isFeatured": true, "isFeatured": true,
"featuredImage": "/uploads/blog/work-visa-canada-main.jpg", "featuredImage": "/assets/img/inner-page/news-details/details-1.jpg",
"galleryImages": [ "galleryImages": [
"/uploads/blog/work-visa-canada-1.jpg", "/assets/img/inner-page/news-details/details-2.jpg",
"/uploads/blog/work-visa-canada-2.jpg" "/assets/img/inner-page/news-details/details-3.jpg"
], ],
"quote": "This blog really helped me understand the difference between student and work visas. The explanations were clear and practical.",
"contentAfterQuote": "<p class=\"mb-3\">It provides access to world-class universities, cultural exposure, and global networking opportunities. With a student visa, you may also get part-time work rights, which can help support your expenses and give you valuable international work experience. However, the primary focus remains on academics and personal growth. On the other hand, a work visa is perfect for those who want to establish themselves in a career overseas.</p><p>It provides immediate access to job markets, stable income, and often a pathway to permanent residency. Work visas are suitable for skilled professionals who are ready to contribute to the global workforce and achieve long-term career goals. Ultimately, the choice comes down to your personal aspirations. If education and exploration are your priorities, a student visa is ideal. If career advancement and stability are your goals, a work visa is the right fit.</p>",
"commentsCount": 3 "commentsCount": 3
}, },
{ {
"title": "Top 5 Scholarship Programs For International Students", "title": "Top 5 Scholarship Programs For International Students",
"slug": "top-5-scholarship-programs-international-students", "slug": "top-5-scholarship-programs-international-students",
"excerpt": "Danh sách 5 chương trình học bổng nổi bật dành cho sinh viên quốc tế với mức hỗ trợ hấp dẫn.", "excerpt": "Danh sách 5 chương trình học bổng nổi bật dành cho sinh viên quốc tế với mức hỗ trợ hấp dẫn.",
"content": "<p>Nếu bạn đang tìm kiếm học bổng để giảm chi phí du học, đây là 5 chương trình bạn không nên bỏ qua. Mỗi chương trình đều có tiêu chí xét tuyển, mức hỗ trợ và thời hạn đăng ký khác nhau.</p>", "content": "<p>Nếu bạn đang tìm kiếm học bổng để giảm chi phí du học, đây là 5 chương trình bạn không nên bỏ qua. Mỗi chương trình đều có tiêu chí xét tuyển, mức hỗ trợ và thời hạn đăng ký khác nhau.</p><p class=\"mt-4 mb-3\">Học bổng là một trong những cách tốt nhất để giảm gánh nặng tài chính khi du học. Các chương trình học bổng không chỉ hỗ trợ về mặt tài chính mà còn mở ra nhiều cơ hội phát triển nghề nghiệp và mở rộng mạng lưới quan hệ quốc tế.</p>",
"category": ["Study Abroad"], "category": ["Study Abroad"],
"tags": ["StudentVisa", "Scholarship"], "tags": ["StudentVisa", "Scholarship"],
"author": "Admin", "author": "Admin",
"status": "published", "status": "published",
"publishedAt": "20 March 2025", "publishedAt": "20 March 2025",
"isFeatured": false, "isFeatured": false,
"featuredImage": "/uploads/blog/scholarship-programs-main.jpg", "featuredImage": "/assets/img/inner-page/news-details/details-1.jpg",
"galleryImages": [], "galleryImages": [
"commentsCount": 0 "/assets/img/inner-page/news-details/details-2.jpg",
"/assets/img/inner-page/news-details/details-3.jpg"
],
"quote": "These scholarship programs opened doors I never thought possible. The application process was straightforward, and the support I received was incredible.",
"contentAfterQuote": "<p class=\"mb-3\">Applying for scholarships requires careful planning and preparation. Start by researching each program's requirements, deadlines, and eligibility criteria. Make sure to prepare all necessary documents well in advance, including transcripts, recommendation letters, and personal statements. Each scholarship has its own unique focus, so tailor your application to highlight how you align with their values and goals.</p><p>Remember that scholarship applications are competitive, so it's important to stand out. Showcase your academic achievements, extracurricular activities, and community involvement. Be authentic in your personal statement and demonstrate how the scholarship will help you achieve your educational and career aspirations. With dedication and proper preparation, you can increase your chances of securing financial support for your studies abroad.</p>",
"commentsCount": 2
}, },
{ {
"title": "10 Travel Safety Tips You Should Know Before Flying", "title": "10 Travel Safety Tips You Should Know Before Flying",
"slug": "10-travel-safety-tips-before-flying", "slug": "10-travel-safety-tips-before-flying",
"excerpt": "Những lưu ý quan trọng để đảm bảo an toàn cho chuyến bay và hành trình của bạn.", "excerpt": "Những lưu ý quan trọng để đảm bảo an toàn cho chuyến bay và hành trình của bạn.",
"content": "<p>An toàn luôn là ưu tiên hàng đầu khi đi du lịch. Dưới đây là 10 tips giúp bạn yên tâm hơn trên mọi chuyến đi, từ việc chuẩn bị giấy tờ, bảo hiểm đến cách bảo vệ tài sản cá nhân.</p>", "content": "<p>An toàn luôn là ưu tiên hàng đầu khi đi du lịch. Dưới đây là 10 tips giúp bạn yên tâm hơn trên mọi chuyến đi, từ việc chuẩn bị giấy tờ, bảo hiểm đến cách bảo vệ tài sản cá nhân.</p><p class=\"mt-4 mb-3\">Du lịch là một trải nghiệm tuyệt vời, nhưng điều quan trọng là phải chuẩn bị kỹ lưỡng để đảm bảo an toàn. Những tips này được đúc kết từ kinh nghiệm thực tế của nhiều du khách và sẽ giúp bạn tránh được những rủi ro không đáng có.</p>",
"category": ["Travel Tips"], "category": ["Travel Tips"],
"tags": ["TravelSafety"], "tags": ["TravelSafety"],
"author": "Admin", "author": "Admin",
"status": "published", "status": "published",
"publishedAt": "05 April 2025", "publishedAt": "05 April 2025",
"isFeatured": false, "isFeatured": false,
"featuredImage": "/uploads/blog/travel-safety-main.jpg", "featuredImage": "/assets/img/inner-page/news-details/details-1.jpg",
"galleryImages": [ "galleryImages": [
"/uploads/blog/travel-safety-1.jpg" "/assets/img/inner-page/news-details/details-2.jpg",
"/assets/img/inner-page/news-details/details-3.jpg"
], ],
"quote": "These safety tips saved me from potential problems during my trip. I especially appreciated the advice about travel insurance and document preparation.",
"contentAfterQuote": "<p class=\"mb-3\">Before you travel, make sure to research your destination thoroughly. Understand local customs, laws, and potential safety concerns. Keep copies of important documents like your passport, visa, and travel insurance in multiple places - both physical and digital. Inform family or friends about your itinerary and check in regularly during your trip.</p><p>When packing, prioritize essential items and keep valuables secure. Use luggage locks and consider travel insurance for expensive items. Stay aware of your surroundings, especially in crowded areas, and trust your instincts if something feels off. By following these safety tips, you can focus on enjoying your journey while staying protected throughout your travels.</p>",
"commentsCount": 1 "commentsCount": 1
} }
], ],
@@ -94,19 +104,19 @@
{ {
"title": "Ultimate Guide To Getting A Work Visa In Canada", "title": "Ultimate Guide To Getting A Work Visa In Canada",
"slug": "ultimate-guide-work-visa-canada", "slug": "ultimate-guide-work-visa-canada",
"thumbnail": "/uploads/blog/work-visa-canada-main.jpg", "thumbnail": "/assets/img/inner-page/news-details/post-1.jpg",
"publishedAt": "11 March 2025" "publishedAt": "11 March 2025"
}, },
{ {
"title": "Top 5 Scholarship Programs For International Students", "title": "Top 5 Scholarship Programs For International Students",
"slug": "top-5-scholarship-programs-international-students", "slug": "top-5-scholarship-programs-international-students",
"thumbnail": "/uploads/blog/scholarship-programs-main.jpg", "thumbnail": "/assets/img/inner-page/news-details/post-2.jpg",
"publishedAt": "20 March 2025" "publishedAt": "20 March 2025"
}, },
{ {
"title": "10 Travel Safety Tips You Should Know Before Flying", "title": "10 Travel Safety Tips You Should Know Before Flying",
"slug": "10-travel-safety-tips-before-flying", "slug": "10-travel-safety-tips-before-flying",
"thumbnail": "/uploads/blog/travel-safety-main.jpg", "thumbnail": "/assets/img/inner-page/news-details/post-3.jpg",
"publishedAt": "05 April 2025" "publishedAt": "05 April 2025"
} }
], ],
@@ -129,6 +139,24 @@
"status": "approved", "status": "approved",
"parentAuthorName": "Frank Flores" "parentAuthorName": "Frank Flores"
}, },
{
"postSlug": "top-5-scholarship-programs-international-students",
"authorName": "Sarah Johnson",
"authorAvatar": "/assets/img/inner-page/news-details/comment-1.png",
"content": "Cảm ơn bạn đã chia sẻ thông tin về các chương trình học bổng này. Mình đã apply và đang chờ kết quả!",
"createdAt": "March 15, 2025",
"status": "approved",
"parentAuthorName": null
},
{
"postSlug": "top-5-scholarship-programs-international-students",
"authorName": "Michael Chen",
"authorAvatar": "/assets/img/inner-page/news-details/comment-2.png",
"content": "Bài viết rất chi tiết và hữu ích. Mình đã tìm thấy một chương trình phù hợp với mình.",
"createdAt": "March 18, 2025",
"status": "approved",
"parentAuthorName": null
},
{ {
"postSlug": "10-travel-safety-tips-before-flying", "postSlug": "10-travel-safety-tips-before-flying",
"authorName": "Jenny Wilson", "authorName": "Jenny Wilson",

View File

@@ -71,6 +71,20 @@ const blogSchema = new mongoose.Schema({
commentsCount: { commentsCount: {
type: Number, type: Number,
default: 0 default: 0
},
// Quote/Sidebar section
quote: {
type: String,
default: '',
trim: true
},
// Content after quote
contentAfterQuote: {
type: String,
default: '',
trim: true
} }
}, { }, {
timestamps: true timestamps: true

View File

@@ -365,6 +365,7 @@ router.post("/blog/categories/create", ensureAuthenticated, blogCategoryControll
router.get("/blog/categories/:id/edit", ensureAuthenticated, blogCategoryController.edit); router.get("/blog/categories/:id/edit", ensureAuthenticated, blogCategoryController.edit);
router.post("/blog/categories/:id/edit", ensureAuthenticated, blogCategoryController.update); router.post("/blog/categories/:id/edit", ensureAuthenticated, blogCategoryController.update);
router.post("/blog/categories/:id/delete", ensureAuthenticated, blogCategoryController.destroy); router.post("/blog/categories/:id/delete", ensureAuthenticated, blogCategoryController.destroy);
router.post("/blog/categories/quick-create", ensureAuthenticated, blogCategoryController.quickCreate);
// Blog Tags Management // Blog Tags Management
router.get("/blog/tags", ensureAuthenticated, blogTagController.index); router.get("/blog/tags", ensureAuthenticated, blogTagController.index);
@@ -373,5 +374,6 @@ router.post("/blog/tags/create", ensureAuthenticated, blogTagController.store);
router.get("/blog/tags/:id/edit", ensureAuthenticated, blogTagController.edit); router.get("/blog/tags/:id/edit", ensureAuthenticated, blogTagController.edit);
router.post("/blog/tags/:id/edit", ensureAuthenticated, blogTagController.update); router.post("/blog/tags/:id/edit", ensureAuthenticated, blogTagController.update);
router.post("/blog/tags/:id/delete", ensureAuthenticated, blogTagController.destroy); router.post("/blog/tags/:id/delete", ensureAuthenticated, blogTagController.destroy);
router.post("/blog/tags/quick-create", ensureAuthenticated, blogTagController.quickCreate);
module.exports = router; module.exports = router;

View File

@@ -144,6 +144,9 @@ router.get("/api/blog/tags/:slug", blogTagController.apiShow);
router.get("/api/blog/:id/categories", blogController.apiCategories); router.get("/api/blog/:id/categories", blogController.apiCategories);
router.get("/api/blog/:id/tags", blogController.apiTags); router.get("/api/blog/:id/tags", blogController.apiTags);
// Blog comments (must come before /api/blog/:slug)
router.post("/api/blog/:slug/comments", blogController.apiCreateComment);
// Blog detail by slug (must come last among blog routes) // Blog detail by slug (must come last among blog routes)
router.get("/api/blog/:slug", blogController.apiShow); router.get("/api/blog/:slug", blogController.apiShow);

View File

@@ -43,6 +43,23 @@ app.use(
express.static(path.join(__dirname, "assets")), express.static(path.join(__dirname, "assets")),
); );
// Serve static files from public directory (uploads, etc.)
app.use(
"/uploads",
(req, res, next) => {
// Cho phép mọi domain truy cập tài nguyên tĩnh
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Methods", "GET");
next();
},
express.static(path.join(__dirname, "public", "uploads")),
);
// Serve other public files
app.use(
express.static(path.join(__dirname, "public")),
);
// Session configuration // Session configuration
app.use( app.use(
session({ session({

1008
views/admin/blog/create.ejs Normal file

File diff suppressed because it is too large Load Diff

1142
views/admin/blog/edit.ejs Normal file

File diff suppressed because it is too large Load Diff

300
views/admin/blog/index.ejs Normal file
View File

@@ -0,0 +1,300 @@
<div class="container">
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
<div>
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
<%= title %>
</h1>
<p class="text-muted mb-0">Manage blog posts and articles</p>
</div>
<div class="d-flex gap-2">
<% if (typeof frontendUrl !=='undefined' ) { %>
<a href="<%= frontendUrl %>/blog" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>View Blog Page
</a>
<% } %>
<a href="/admin/blog/create" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Create New Post
</a>
</div>
</div>
<!-- Filters -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="GET" action="/admin/blog" class="row g-3">
<div class="col-md-3">
<label class="form-label">Search</label>
<input type="text" class="form-control" name="search" value="<%= query.search || '' %>"
placeholder="Search title or excerpt...">
</div>
<div class="col-md-2">
<label class="form-label">Status</label>
<select class="form-select" name="status">
<option value="">All</option>
<option value="published" <%=query.status==='published' ? 'selected' : '' %>>Published</option>
<option value="draft" <%=query.status==='draft' ? 'selected' : '' %>>Draft</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Category</label>
<select class="form-select" name="category">
<option value="">All Categories</option>
<% categories.forEach(cat=> { %>
<option value="<%= cat.name %>" <%=query.category===cat.name ? 'selected' : '' %>>
<%= cat.name %>
</option>
<% }); %>
</select>
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-search me-1"></i>Filter
</button>
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<a href="/admin/blog" class="btn btn-outline-secondary w-100">
<i class="fas fa-times me-1"></i>Clear
</a>
</div>
</form>
</div>
</div>
<!-- Blog List -->
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<% if (blogs && blogs.length> 0) { %>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th scope="col" style="width: 60px">Image</th>
<th scope="col">Title</th>
<th scope="col">Category</th>
<th scope="col">Status</th>
<th scope="col">Author</th>
<th scope="col">Published</th>
<th scope="col" style="width: 200px">Actions</th>
</tr>
</thead>
<tbody>
<% blogs.forEach((blog, index)=> { %>
<tr>
<td>
<% if (blog.featuredImage) { %>
<img src="<%= blog.featuredImage.startsWith('http') ? blog.featuredImage : (typeof frontendUrl !== 'undefined' ? frontendUrl : '') + blog.featuredImage %>"
alt="<%= blog.title %>" class="img-thumbnail"
style="width: 50px; height: 50px; object-fit: cover;">
<% } else { %>
<div class="bg-light d-flex align-items-center justify-content-center"
style="width: 50px; height: 50px;">
<i class="fas fa-image text-muted"></i>
</div>
<% } %>
</td>
<td>
<div>
<span class="fw-medium">
<%= blog.title %>
</span>
<% if (blog.isFeatured) { %>
<span class="badge bg-warning text-dark ms-2">Featured</span>
<% } %>
</div>
<small class="text-muted">
<%= blog.excerpt.substring(0, 60) %>...
</small>
</td>
<td>
<% if (blog.category && blog.category.length> 0) { %>
<% blog.category.slice(0, 2).forEach(cat=> { %>
<span class="badge bg-secondary me-1">
<%= cat %>
</span>
<% }); %>
<% if (blog.category.length> 2) { %>
<span class="text-muted">+<%= blog.category.length - 2 %></span>
<% } %>
<% } else { %>
<span class="text-muted">-</span>
<% } %>
</td>
<td>
<% if (blog.status==='published' ) { %>
<span class="badge bg-success">Published</span>
<% } else { %>
<span class="badge bg-secondary">Draft</span>
<% } %>
</td>
<td>
<%= blog.author || 'Admin' %>
</td>
<td>
<%= blog.publishedAt || '-' %>
</td>
<td>
<div class="btn-group" role="group">
<% if (typeof frontendUrl !== 'undefined') { %>
<a href="<%= frontendUrl %>/blog/<%= blog.slug %>" target="_blank"
class="btn btn-sm btn-outline-secondary">
<i class="fas fa-external-link-alt me-1"></i>View
</a>
<% } %>
<a href="/admin/blog/<%= blog._id %>/edit"
class="btn btn-sm btn-outline-primary">
<i class="fas fa-edit me-1"></i>Edit
</a>
<button type="button" class="btn btn-sm btn-outline-danger"
data-custom-modal="open" data-id="<%= blog._id %>"
data-title="<%= blog.title %>">
<i class="fas fa-trash-alt me-1"></i>Delete
</button>
</div>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<!-- Pagination -->
<% if (pagination && pagination.total> 1) { %>
<nav aria-label="Blog pagination" class="mt-4">
<ul class="pagination justify-content-center">
<% if (pagination.current> 1) { %>
<li class="page-item">
<a class="page-link"
href="?page=<%= pagination.current - 1 %><%= query.search ? '&search=' + query.search : '' %><%= query.status ? '&status=' + query.status : '' %><%= query.category ? '&category=' + query.category : '' %>">Previous</a>
</li>
<% } %>
<% for (let i=1; i <=pagination.total; i++) { %>
<% if (i===pagination.current) { %>
<li class="page-item active">
<span class="page-link">
<%= i %>
</span>
</li>
<% } else if (i===1 || i===pagination.total || (i>= pagination.current - 2
&& i <= pagination.current + 2)) { %>
<li class="page-item">
<a class="page-link"
href="?page=<%= i %><%= query.search ? '&search=' + query.search : '' %><%= query.status ? '&status=' + query.status : '' %><%= query.category ? '&category=' + query.category : '' %>">
<%= i %>
</a>
</li>
<% } else if (i===pagination.current - 3 || i===pagination.current +
3) { %>
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
<% } %>
<% } %>
<% if (pagination.current < pagination.total) { %>
<li class="page-item">
<a class="page-link"
href="?page=<%= pagination.current + 1 %><%= query.search ? '&search=' + query.search : '' %><%= query.status ? '&status=' + query.status : '' %><%= query.category ? '&category=' + query.category : '' %>">Next</a>
</li>
<% } %>
</ul>
</nav>
<% } %>
<% } else { %>
<div class="text-center py-5">
<i class="fas fa-blog text-muted mb-3" style="font-size: 3rem;"></i>
<h5 class="text-muted mb-3">No Blog Posts Found</h5>
<a href="/admin/blog/create" class="btn btn-primary">
<i class="fas fa-plus-circle me-1"></i>Create First Blog Post
</a>
</div>
<% } %>
</div>
</div>
</div>
<!-- Delete Blog Confirmation Modal -->
<div class="modal fade" id="deleteBlogModal" tabindex="-1" aria-labelledby="deleteBlogModalLabel" aria-hidden="true"
data-bs-backdrop="true" data-bs-keyboard="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="deleteBlogModalLabel">
<i class="fas fa-trash me-2"></i>Confirm Delete
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the blog post "<span id="deleteBlogTitle" class="fw-bold"></span>"?
</p>
<p class="text-danger mb-0">
<small>
<i class="fas fa-exclamation-triangle me-1"></i>This action cannot be
undone.</small>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<form id="deleteBlogForm" method="POST" style="display: inline;">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash me-1"></i>Delete
</button>
</form>
</div>
</div>
</div>
</div>
<style>
/* Fix modal z-index - must be higher than everything else */
#deleteBlogModal {
z-index: 2050 !important;
}
#deleteBlogModal .modal-dialog {
z-index: 2060 !important;
position: relative;
}
#deleteBlogModal .modal-content {
z-index: 2070 !important;
position: relative;
}
/* Ensure modal is clickable */
#deleteBlogModal.show {
display: block !important;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function () { // Initialize modal instance once
const deleteModalElement = document.getElementById('deleteBlogModal');
const deleteModal = new bootstrap.Modal(deleteModalElement, {
backdrop: false,
keyboard: true,
focus: true
});
// Handle delete buttons
document.querySelectorAll('[data-custom-modal="open"]').forEach(button => {
button.addEventListener('click', function (e) {
e.preventDefault();
const blogId = this.getAttribute('data-id');
const blogTitle = this.getAttribute('data-title');
// Set blog title in modal
document.getElementById('deleteBlogTitle').textContent = blogTitle;
// Set form action
document.getElementById('deleteBlogForm').action = `/admin/blog/${blogId}/delete`;
// Show Bootstrap modal
deleteModal.show();
});
});
});
</script>

View File

@@ -1,122 +1,117 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<link rel="icon" type="image/png" href="/uploads/layout/favicon.png" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>
<%= typeof title !=='undefined' ? title + ' | ' : '' %>CMS-GGCamp
</title>
<!-- Bootstrap CSS -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<!-- Font Awesome -->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
/>
<style>
:root {
--primary-color: #b8b76a;
--primary-light: #c9c88a;
--primary-dark: #9a994a;
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
.nav-link:hover {
color: var(--primary-color);
}
.nav-link.active {
color: var(--primary-color);
font-weight: 600;
}
</style>
<%- style %>
</head>
<body> <head>
<div class="container-fluid"> <link rel="icon" type="image/png" href="/uploads/layout/favicon.png" />
<div class="row"> <meta charset="UTF-8" />
<nav class="col-md-2 d-none d-md-block bg-light sidebar"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<div class="position-sticky pt-3"> <title>
<ul class="nav flex-column"> <%= typeof title !=='undefined' ? title + ' | ' : '' %>CMS-GGCamp
<li class="nav-item"> </title>
<a class="nav-link" href="/admin/dashboard">Dashboard</a> <!-- Bootstrap CSS -->
</li> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<li class="nav-item"> <!-- Font Awesome -->
<a class="nav-link" href="/admin/home">Home</a> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
</li> <style>
<li class="nav-item"> :root {
<a class="nav-link" href="/admin/header">Header & Menu</a> --primary-color: #b8b76a;
</li> --primary-light: #c9c88a;
<li class="nav-item"> --primary-dark: #9a994a;
<a class="nav-link" href="/admin/footer">Footer</a> }
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/about">About</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/about-us">About Us</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/contact">Contact</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/faq">FAQ</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/terms-conditions">Terms & Conditions</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/safety">Safety</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/camp-location">Camp Location</a>
</li>
<li class="nav-item">
<li class="nav-item">
<a class="nav-link" href="/admin/form">Form</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/upload">Upload</a>
</li>
</ul>
</div>
</nav>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4"> .btn-primary {
<!-- Flash Messages Data (Hidden) --> background-color: var(--primary-color);
<% if(typeof success_msg !=='undefined' || typeof error_msg border-color: var(--primary-color);
!=='undefined' || typeof error !=='undefined' ) { %> color: white;
}
.btn-primary:hover {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
.nav-link:hover {
color: var(--primary-color);
}
.nav-link.active {
color: var(--primary-color);
font-weight: 600;
}
</style>
<%- style %>
</head>
<body>
<div class="container-fluid">
<div class="row">
<nav class="col-md-2 d-none d-md-block bg-light sidebar">
<div class="position-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="/admin/dashboard">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/home">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/header">Header & Menu</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/footer">Footer</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/about">About</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/about-us">About Us</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/contact">Contact</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/faq">FAQ</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/terms-conditions">Terms & Conditions</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/safety">Safety</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/camp-location">Camp Location</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/blog">Blog</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/form">Form</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/upload">Upload</a>
</li>
</ul>
</div>
</nav>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<!-- Flash Messages Data (Hidden) -->
<% if(typeof success_msg !=='undefined' || typeof error_msg !=='undefined' || typeof error !=='undefined' ) { %>
<div id="flash-messages-data" style="display: none"> <div id="flash-messages-data" style="display: none">
<%- JSON.stringify({ success_msg: typeof success_msg !=='undefined' <%- JSON.stringify({ success_msg: typeof success_msg !=='undefined' && success_msg ? success_msg : null,
&& success_msg ? success_msg : null, error_msg: typeof error_msg error_msg: typeof error_msg !=='undefined' && error_msg ? error_msg : null, error: typeof error
!=='undefined' && error_msg ? error_msg : null, error: typeof error !=='undefined' && error ? error : null }) %>
!=='undefined' && error ? error : null }) %>
</div> </div>
<% } %> <% } %>
<div class="py-3"><%- body %></div> <div class="py-3"><%- body %></div>
</main> </main>
</div>
</div> </div>
</div>
<!-- Bootstrap JS --> <!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
<%- script %> <%- script %>
</body> </body>
</html>
</html>

View File

@@ -727,91 +727,67 @@
About About
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li> <li>
<a <a class="dropdown-item <%= currentPath === '/admin/about-us' ? 'active' : '' %>"
class="dropdown-item <%= currentPath === '/admin/about-us' ? 'active' : '' %>" href="/admin/about-us">About us</a>
href="/admin/about-us" </li>
>About us</a <li>
> <a class="dropdown-item <%= currentPath === '/admin/safety' ? 'active' : '' %>"
</li> href="/admin/safety">Safety</a>
<li> </li>
<a <li>
class="dropdown-item <%= currentPath === '/admin/safety' ? 'active' : '' %>" <a class="dropdown-item <%= currentPath === '/admin/FAQ' ? 'active' : '' %>" href="/admin/faq">FAQ</a>
href="/admin/safety" </li>
>Safety</a <li>
> <a class="dropdown-item <%= currentPath === '/admin/insurance' ? 'active' : '' %>"
</li> href="/admin/insurance">Insurance</a>
<li> </li>
<a
class="dropdown-item <%= currentPath === '/admin/FAQ' ? 'active' : '' %>" <li>
href="/admin/faq" <a class="dropdown-item <%= currentPath === '/admin/travel' ? 'active' : '' %>"
>FAQ</a href="/admin/travel">Travel</a>
> </li>
</li> <li>
<li> <a class="dropdown-item <%= currentPath === '/admin/terms-conditions' ? 'active' : '' %>"
<a href="/admin/terms-conditions">Terms & Conditions</a>
class="dropdown-item <%= currentPath === '/admin/insurance' ? 'active' : '' %>" </li>
href="/admin/insurance"
>Insurance</a </ul>
> </li>
</li> <li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/contact' ? 'active' : '' %>" href="/admin/contact">Contact
Us</a>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/camp-location' ? 'active' : '' %>"
href="/admin/camp-location">Camp Location</a>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/activity' ? 'active' : '' %>" href="/admin/activity">Activity
& Booking</a>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/blog' || currentPath.startsWith('/admin/blog') ? 'active' : '' %>"
href="/admin/blog">Blog</a>
</li>
</ul>
<li>
<a
class="dropdown-item <%= currentPath === '/admin/travel' ? 'active' : '' %>"
href="/admin/travel"
>Travel</a
>
</li>
<li>
<a
class="dropdown-item <%= currentPath === '/admin/terms-conditions' ? 'active' : '' %>"
href="/admin/terms-conditions"
>Terms & Conditions</a
>
</li>
</ul>
</li>
<li class="nav-item">
<a
class="nav-link <%= currentPath === '/admin/contact' ? 'active' : '' %>"
href="/admin/contact"
>Contact Us</a
>
</li>
<li class="nav-item">
<a
class="nav-link <%= currentPath === '/admin/camp-location' ? 'active' : '' %>"
href="/admin/camp-location"
>Camp Location</a
>
</li>
<li class="nav-item">
<a
class="nav-link <%= currentPath === '/admin/activity' ? 'active' : '' %>"
href="/admin/activity"
>Activity & Booking</a
>
</li>
</ul>
</ul> </ul>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<% if (locals.user) { %> <% if (locals.user) { %>
<a href="/admin/dashboard" class="btn btn-primary me-2"> <a href="/admin/dashboard" class="btn btn-primary me-2">
<i class="fas fa-tachometer-alt me-1"></i>Dashboard <i class="fas fa-tachometer-alt me-1"></i>Dashboard
</a> </a>
<a href="/auth/logout" class="btn btn-outline-danger"> <a href="/auth/logout" class="btn btn-outline-danger">
<i class="fas fa-sign-out-alt me-1"></i>Logout <i class="fas fa-sign-out-alt me-1"></i>Logout
</a> </a>
<% } else { %> <% } else { %>
<a href="/auth/login" class="btn btn-outline-primary"> <a href="/auth/login" class="btn btn-outline-primary">
<i class="fas fa-sign-in-alt me-1"></i>Login <i class="fas fa-sign-in-alt me-1"></i>Login
</a> </a>
<% } %> <% } %>
</div> </div>
</div> </div>
</div> </div>
@@ -821,12 +797,12 @@
<main> <main>
<!-- Flash Messages Data (Hidden) --> <!-- Flash Messages Data (Hidden) -->
<% if(typeof success_msg !=='undefined' || typeof error_msg !=='undefined' || typeof error !=='undefined' ) { %> <% if(typeof success_msg !=='undefined' || typeof error_msg !=='undefined' || typeof error !=='undefined' ) { %>
<div id="flash-messages-data" style="display: none"> <div id="flash-messages-data" style="display: none">
<%- JSON.stringify({ success_msg: typeof success_msg !=='undefined' && success_msg ? success_msg : null, <%- JSON.stringify({ success_msg: typeof success_msg !=='undefined' && success_msg ? success_msg : null,
error_msg: typeof error_msg !=='undefined' && error_msg ? error_msg : null, error: typeof error !=='undefined' error_msg: typeof error_msg !=='undefined' && error_msg ? error_msg : null, error: typeof error !=='undefined'
&& error ? error : null }) %> && error ? error : null }) %>
</div> </div>
<% } %> <%- body %> <% } %> <%- body %>
</main> </main>
<!-- Footer --> <!-- Footer -->
@@ -841,14 +817,14 @@
<h5 class="mb-3">Links</h5> <h5 class="mb-3">Links</h5>
<ul class="list-unstyled"> <ul class="list-unstyled">
<% if (locals.user) { %> <% if (locals.user) { %>
<li class="mb-2"> <li class="mb-2">
<a href="/admin/dashboard" class="text-decoration-none hover-opacity">Dashboard</a> <a href="/admin/dashboard" class="text-decoration-none hover-opacity">Dashboard</a>
</li> </li>
<% } else { %> <% } else { %>
<li class="mb-2"> <li class="mb-2">
<a href="/auth/login" class="text-decoration-none hover-opacity">Login</a> <a href="/auth/login" class="text-decoration-none hover-opacity">Login</a>
</li> </li>
<% } %> <% } %>
</ul> </ul>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
@@ -865,7 +841,7 @@
<div class="text-center"> <div class="text-center">
<p class="mb-0"> <p class="mb-0">
&copy; <%= new Date().getFullYear() %> CMS-GGCamp. All rights &copy; <%= new Date().getFullYear() %> CMS-GGCamp. All rights
reserved. reserved.
</p> </p>
</div> </div>
</div> </div>
@@ -928,7 +904,7 @@
formattedType + '</span><i class="fas fa-chevron-right nav-submenu-indicator"></i>'; formattedType + '</span><i class="fas fa-chevron-right nav-submenu-indicator"></i>';
if (window.location.pathname.includes("/admin/level") && window.location.search.includes( if (window.location.pathname.includes("/admin/level") && window.location.search.includes(
"type=" + type)) { "type=" + type)) {
link.classList.add("active"); link.classList.add("active");
} }