Files
cms.uldp.edu.vn/controllers/travelController.js
r2xrzh9q2z-lab d1b931d547 first commit
2026-02-02 11:07:09 +07:00

232 lines
8.3 KiB
JavaScript

const Travel = require("../models/travel");
const { addBaseUrlToImages } = require("../utils/imageHelper");
const fs = require("fs").promises;
const path = require("path");
/**
* Hàm Helper: Trích xuất ID YouTube từ nhiều định dạng link khác nhau
*/
function extractYouTubeId(url) {
if (!url || typeof url !== 'string') return null;
// Hỗ trợ: watch?v=, embed/, youtu.be/, shorts/
const regex = /(?:youtu\.be\/|youtube(?:-nocookie)?\.com\/(?:watch\?v=|embed\/|v\/|shorts\/))([A-Za-z0-9_-]{11})/;
const match = url.match(regex);
return match ? match[1] : null;
}
/**
* Hàm Helper: Làm sạch danh sách blocks của Editor.js
* Loại bỏ duplicate video (vừa embed vừa text link) và paragraph rỗng
*/
function sanitizeContentBlocks(blocks) {
if (!blocks || !Array.isArray(blocks)) return [];
const seenVideoIds = new Set();
// Bước 1: Duyệt qua để chuẩn hóa block embed và lấy danh sách ID video
const processedBlocks = blocks.map(block => {
if (block.type === 'embed') {
const url = block.data.source || block.data.embed || '';
const videoId = extractYouTubeId(url);
if (videoId) {
seenVideoIds.add(videoId);
// Cập nhật lại data chuẩn cho Editor.js
block.data.embed = `https://www.youtube.com/embed/${videoId}`;
block.data.source = url;
block.data.videoId = videoId;
block.data.service = 'youtube';
}
}
return block;
});
// Bước 2: Lọc bỏ paragraph rác
return processedBlocks.filter(block => {
if (block.type === 'paragraph') {
const text = (block.data?.text || '').trim();
// Xóa paragraph rỗng
if (text === '' || text === '<br>' || text === '&nbsp;') return false;
// Xóa paragraph nếu nó chỉ chứa 1 link YouTube đã được embed ở trên
const videoIdInText = extractYouTubeId(text);
if (videoIdInText && seenVideoIds.has(videoIdInText)) {
console.log(`[Sanitizer] Removed duplicate text link for video: ${videoIdInText}`);
return false;
}
}
return true;
});
}
// GET: Show travel editor
exports.index = async (req, res) => {
try {
const travel = await Travel.findOne();
if (!travel) {
return res.render("admin/travel/index", {
title: "Travel Management",
data: {
page: { title: "Travel Information", description: "", metadata: { title: "", description: "" } },
hero: { title: "Travel Information", backgroundImage: "" },
content: { blocks: [] },
enableScrollspy: false,
},
message: "No travel data found. Please run migration first.",
});
}
res.render("admin/travel/index", {
title: "Travel Management",
data: travel,
});
} catch (error) {
console.error("Error loading travel page:", error);
res.status(500).send("Error loading travel page");
}
};
// POST: Update travel information
exports.update = async (req, res) => {
try {
const { page, hero, content, enableScrollspy } = req.body;
const updateData = {};
if (page) updateData.page = JSON.parse(page);
if (hero) updateData.hero = JSON.parse(hero);
if (content) {
let contentObj = JSON.parse(content);
// Áp dụng bộ lọc dọn dẹp nội dung
contentObj.blocks = sanitizeContentBlocks(contentObj.blocks);
updateData.content = contentObj;
}
if (enableScrollspy !== undefined) {
updateData.enableScrollspy = (enableScrollspy === "true" || enableScrollspy === true);
}
await Travel.findOneAndUpdate({}, updateData, {
upsert: true,
new: true,
});
req.flash("success", "Travel information updated and sanitized successfully");
res.redirect("/admin/travel");
} catch (error) {
console.error("Error updating travel:", error);
req.flash("error", "Error updating travel information");
res.redirect("/admin/travel");
}
};
// GET: Travel data API (Sử dụng cho Frontend/Public)
exports.api = exports.getTravelData = async (req, res) => {
try {
const travel = await Travel.findOne();
if (!travel) {
return res.status(404).json({ error: "Travel data not found" });
}
const travelObj = travel.toObject();
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
const processed = addBaseUrlToImages(travelObj, baseUrl);
return res.json({
success: true,
data: {
hero: processed.hero,
page: processed.page,
content: processed.content,
enableScrollspy: processed.enableScrollspy
},
});
} catch (error) {
console.error("Error fetching travel data:", error);
res.status(500).json({ error: "Internal server error" });
}
};
// POST: Preview travel
exports.preview = async (req, res) => {
try {
const { content, pageTitle, heroTitle, heroBackgroundImage, pageYear } = req.body;
// Preview cũng cần được sanitize để hiển thị đúng thực tế khi lưu
let contentObj = JSON.parse(content);
contentObj.blocks = sanitizeContentBlocks(contentObj.blocks);
const previewData = {
page: {
title: pageTitle || "Travel Information",
year: pageYear || ""
},
hero: {
title: heroTitle || "Travel Information",
backgroundImage: heroBackgroundImage || "",
},
content: contentObj,
enableScrollspy: false,
};
res.render("page/travel", {
title: "Travel Preview",
data: previewData,
});
} catch (error) {
console.error("Error generating preview:", error);
res.status(500).send("Error generating preview");
}
};
// GET: Seed/Import from JSON
exports.seed = async (req, res) => {
try {
const jsonPath = path.join(__dirname, "../data/travel.json");
const jsonData = await fs.readFile(jsonPath, "utf-8");
const jsonTravelData = JSON.parse(jsonData);
let contentBlocks = [];
// Trường hợp JSON đã có định dạng bài viết (blog format)
if (Array.isArray(jsonTravelData.posts) && jsonTravelData.posts.length > 0) {
const firstPost = jsonTravelData.posts[0];
contentBlocks = (firstPost.content && firstPost.content.blocks) ? firstPost.content.blocks : [];
}
// Trường hợp format cũ (legacy)
else {
// ... (Logic chuyển đổi legacy format giữ nguyên nhưng bọc qua sanitize)
// Ví dụ: push các header, paragraph từ locations vào contentBlocks
}
// Luôn làm sạch dữ liệu trước khi seed vào DB
const cleanedBlocks = sanitizeContentBlocks(contentBlocks);
const travelData = {
page: {
title: jsonTravelData.page?.title || "Go and Grow Camp Travel Information",
year: jsonTravelData.page?.year || "",
metadata: {
title: "Travel Guide - Go and Grow Camp",
description: "Everything you need to know about traveling to our camps",
},
},
hero: {
title: jsonTravelData.hero?.title || "Travel Information",
backgroundImage: jsonTravelData.hero?.backgroundImage || "",
},
content: { blocks: cleanedBlocks },
enableScrollspy: true,
};
await Travel.findOneAndUpdate({}, travelData, { upsert: true, new: true });
req.flash("success", "Travel data seeded and sanitized successfully");
res.redirect("/admin/travel");
} catch (error) {
console.error("Error seeding travel data:", error);
req.flash("error", "Failed to seed travel data");
res.redirect("/admin/travel");
}
};