forked from UKSOURCE/cms.hailearning.edu.vn
232 lines
8.3 KiB
JavaScript
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 === ' ') 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");
|
|
}
|
|
}; |