forked from UKSOURCE/cms.hailearning.edu.vn
first commit
This commit is contained in:
536
controllers/termsController.js
Normal file
536
controllers/termsController.js
Normal file
@@ -0,0 +1,536 @@
|
||||
// controllers/termsController.js
|
||||
const Terms = require("../models/terms");
|
||||
const { addBaseUrlToImages } = require("../utils/imageHelper"); // Import helper
|
||||
|
||||
// API để lấy terms data (cho frontend)
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
const language = req.query.lang || "en";
|
||||
|
||||
// Sử dụng getDefault để đảm bảo luôn có data
|
||||
const terms = await Terms.getDefault(language);
|
||||
|
||||
// Trả về data với cấu trúc mới
|
||||
const termsData = terms.toObject();
|
||||
|
||||
// Sử dụng helper để thêm base URL vào đường dẫn ảnh
|
||||
// Truyền baseUrl từ request hoặc từ environment
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const processedData = addBaseUrlToImages(termsData, baseUrl);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
hero: processedData.hero,
|
||||
page: processedData.page,
|
||||
content: processedData.content
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("API Error:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading terms data",
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API để lấy toàn bộ terms data (cho admin)
|
||||
exports.getTermsData = async (req, res) => {
|
||||
try {
|
||||
const language = req.query.lang || "en";
|
||||
const terms = await Terms.findOne({ name: "default", language: language });
|
||||
|
||||
if (!terms) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Terms data not found"
|
||||
});
|
||||
}
|
||||
|
||||
const termsData = terms.toObject();
|
||||
|
||||
// Thêm base URL vào đường dẫn ảnh
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const processedData = addBaseUrlToImages(termsData, baseUrl);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: processedData
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error getting terms data:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading terms data"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API để lấy data theo ngôn ngữ
|
||||
exports.getByLanguage = async (req, res) => {
|
||||
try {
|
||||
const language = req.params.lang || "en";
|
||||
|
||||
const terms = await Terms.findOne({ name: "default", language: language });
|
||||
|
||||
if (!terms) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Terms data not found for language: " + language
|
||||
});
|
||||
}
|
||||
|
||||
const termsData = terms.toObject();
|
||||
|
||||
// Thêm base URL vào đường dẫn ảnh
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const processedData = addBaseUrlToImages(termsData, baseUrl);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
hero: processedData.hero,
|
||||
page: processedData.page,
|
||||
content: processedData.content
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error getting terms by language:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading terms data"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Render admin view (không cần thêm baseUrl ở đây vì dùng trong CMS)
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
// Luôn đảm bảo có default data
|
||||
const terms = await Terms.getDefault("en");
|
||||
const data = terms.toObject();
|
||||
|
||||
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||
|
||||
res.render("admin/terms/index", {
|
||||
title: "Terms & Conditions Management",
|
||||
layout: "layouts/main",
|
||||
data, // Không cần addBaseUrlToImages cho admin view
|
||||
frontendUrl,
|
||||
currentPath: req.path,
|
||||
user: req.session.user
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in terms index:", error);
|
||||
req.flash("error_msg", "An error occurred while loading the page");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
};
|
||||
|
||||
// Cập nhật dữ liệu terms (CẬP NHẬT CẤU TRÚC MỚI)
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const { hero, page, content } = req.body;
|
||||
|
||||
// Parse JSON strings
|
||||
const parseJson = (data) => {
|
||||
if (!data) return null;
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error("JSON parse error:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
// Parse all data với cấu trúc mới
|
||||
const heroData = parseJson(hero) || {};
|
||||
const pageData = parseJson(page) || {};
|
||||
const contentData = parseJson(content) || {};
|
||||
|
||||
// Normalize embed blocks (convert YouTube watch URLs to /embed/ URLs)
|
||||
function extractYouTubeId(url) {
|
||||
if (!url || typeof url !== 'string') return null;
|
||||
// common YouTube URL patterns
|
||||
const m = url.match(/(?:youtu\.be\/|youtube(?:-nocookie)?\.com\/(?:watch\?v=|embed\/|v\/|shorts\/))([A-Za-z0-9_-]{11})/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
// Trong exports.update
|
||||
if (contentData && Array.isArray(contentData.content)) {
|
||||
contentData.content = contentData.content.map(item => {
|
||||
if (item && item.type === 'embed') {
|
||||
let embedUrl = item.embed || item.url || item.source || '';
|
||||
|
||||
// Luôn chuyển đổi sang embed URL nếu là watch URL
|
||||
if (embedUrl.includes('youtube.com/watch')) {
|
||||
const videoId = extractYouTubeId(embedUrl);
|
||||
if (videoId) {
|
||||
item.embed = `https://www.youtube.com/embed/${videoId}`;
|
||||
item.videoId = videoId;
|
||||
}
|
||||
}
|
||||
// Đảm bảo có videoId
|
||||
else if (embedUrl && !item.videoId) {
|
||||
const videoId = extractYouTubeId(embedUrl);
|
||||
if (videoId) {
|
||||
item.videoId = videoId;
|
||||
}
|
||||
}
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
// Tìm hoặc tạo terms
|
||||
let terms = await Terms.findOne({ name: "default", language: "en" });
|
||||
|
||||
if (!terms) {
|
||||
// Tạo mới với cấu trúc mới
|
||||
terms = new Terms({
|
||||
name: "default",
|
||||
language: "en",
|
||||
hero: heroData,
|
||||
page: pageData,
|
||||
content: contentData,
|
||||
version: "2.0.0",
|
||||
isActive: true,
|
||||
migratedFromOldStructure: false
|
||||
});
|
||||
} else {
|
||||
// Update existing với cấu trúc mới
|
||||
terms.hero = heroData;
|
||||
terms.page = pageData;
|
||||
terms.content = contentData;
|
||||
terms.version = "2.0.0";
|
||||
terms.migratedFromOldStructure = false;
|
||||
terms.updatedAt = new Date();
|
||||
}
|
||||
|
||||
await terms.save();
|
||||
|
||||
req.flash("success_msg", "Terms & Conditions updated successfully");
|
||||
res.redirect("/admin/terms-conditions");
|
||||
|
||||
} catch (err) {
|
||||
console.error("Error updating terms:", err);
|
||||
req.flash("error_msg", err.message || "Error updating terms");
|
||||
res.redirect("/admin/terms-conditions");
|
||||
}
|
||||
};
|
||||
|
||||
// Seed data từ JSON file mới (cấu trúc mới)
|
||||
exports.seed = async (req, res) => {
|
||||
try {
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
// Đọc file JSON
|
||||
const jsonPath = path.join(__dirname, '../data/terms-conditions.json');
|
||||
const jsonData = JSON.parse(await fs.readFile(jsonPath, 'utf8'));
|
||||
|
||||
console.log('Seeding from JSON...');
|
||||
console.log('JSON structure keys:', Object.keys(jsonData));
|
||||
|
||||
// Kiểm tra cấu trúc JSON
|
||||
let terms;
|
||||
if (jsonData.hero && jsonData.page && jsonData.content) {
|
||||
// Cấu trúc mới
|
||||
console.log('Using new structure (hero, page, content)');
|
||||
terms = await Terms.migrateFromNewJson(jsonData, "en");
|
||||
} else if (jsonData.hero && jsonData.termsHeader && jsonData.sections) {
|
||||
// Cấu trúc cũ
|
||||
console.log('Using old structure, converting to new...');
|
||||
terms = await Terms.migrateFromJson(jsonData, "en");
|
||||
} else {
|
||||
throw new Error("Unknown JSON structure");
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Terms data seeded successfully",
|
||||
data: {
|
||||
id: terms._id,
|
||||
hero: terms.hero,
|
||||
page: terms.page,
|
||||
content: terms.content
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error seeding terms:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Error seeding terms data"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API preview cho admin (tạo HTML preview)
|
||||
exports.preview = async (req, res) => {
|
||||
try {
|
||||
const { hero, page, content } = req.body;
|
||||
|
||||
// Parse JSON strings
|
||||
const parseJson = (data) => {
|
||||
if (!data) return null;
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error("JSON parse error:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const heroData = parseJson(hero) || {};
|
||||
const pageData = parseJson(page) || {};
|
||||
const contentData = parseJson(content) || {};
|
||||
|
||||
// Thêm base URL vào đường dẫn ảnh cho preview
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const processedHeroData = addBaseUrlToImages(heroData, baseUrl);
|
||||
|
||||
// Render preview HTML
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${pageData.title || 'Terms & Conditions Preview'}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; }
|
||||
.hero-section {
|
||||
background: linear-gradient(rgba(0,0,0,0.7), rgba(0,0,0,0.7)),
|
||||
url('${processedHeroData.backgroundImage || ''}');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
color: white;
|
||||
padding: 100px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.page-header {
|
||||
padding: 40px 20px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
.content-section {
|
||||
padding: 40px 20px;
|
||||
}
|
||||
.content-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Hero Section -->
|
||||
<div class="hero-section">
|
||||
<h1>${heroData.title || 'Terms & Conditions'}</h1>
|
||||
</div>
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<div class="container">
|
||||
<h2>${pageData.title || 'Terms & Conditions'}</h2>
|
||||
${pageData.divider !== false ? '<hr>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Section -->
|
||||
<div class="content-section">
|
||||
<div class="container">
|
||||
${renderContentItems(contentData.content || [])}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
res.send(html);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error generating preview:", error);
|
||||
res.status(500).send("Error generating preview");
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function để render content items
|
||||
function renderContentItems(contentItems) {
|
||||
if (!Array.isArray(contentItems) || contentItems.length === 0) {
|
||||
return '<p>No content available.</p>';
|
||||
}
|
||||
|
||||
return contentItems.map(item => {
|
||||
switch (item.type) {
|
||||
case 'paragraph':
|
||||
return `<div class="content-item"><p>${item.text || ''}</p></div>`;
|
||||
|
||||
case 'section':
|
||||
let html = `<div class="content-item">`;
|
||||
html += `<h3>${item.title || ''}</h3>`;
|
||||
html += `<p>${item.content || ''}</p>`;
|
||||
|
||||
if (item.subsections && item.subsections.length > 0) {
|
||||
item.subsections.forEach(subsection => {
|
||||
if (subsection.type === 'cancellation_table') {
|
||||
html += `<h4>${subsection.title || ''}</h4>`;
|
||||
if (subsection.items && subsection.items.length > 0) {
|
||||
html += '<ul>';
|
||||
subsection.items.forEach(listItem => {
|
||||
html += `<li>${listItem}</li>`;
|
||||
});
|
||||
html += '</ul>';
|
||||
}
|
||||
} else if (subsection.type === 'cancellation_section') {
|
||||
html += `<h4>${subsection.title || ''}</h4>`;
|
||||
if (subsection.items && subsection.items.length > 0) {
|
||||
html += '<ul>';
|
||||
subsection.items.forEach(listItem => {
|
||||
html += `<li>${listItem}</li>`;
|
||||
});
|
||||
html += '</ul>';
|
||||
}
|
||||
} else if (subsection.type === 'note') {
|
||||
html += `<div class="alert alert-info">${subsection.text || ''}</div>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
return html;
|
||||
|
||||
case 'note':
|
||||
return `<div class="content-item alert alert-info">${item.text || ''}</div>`;
|
||||
case 'embed':
|
||||
// Support several embed shapes: { embed }, { url }, { source }, { videoId }
|
||||
const embedSrc = (item.embed || item.url || item.source || (item.videoId ? `https://www.youtube.com/embed/${item.videoId}` : ''));
|
||||
if (!embedSrc) return `<div class="content-item">Invalid embed</div>`;
|
||||
return `<div class="content-item embed-item" style="margin-bottom:20px;">
|
||||
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;">
|
||||
<iframe src="${embedSrc}" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allowfullscreen loading="lazy" referrerpolicy="no-referrer"></iframe>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
default:
|
||||
return `<div class="content-item">Unknown content type: ${item.type}</div>`;
|
||||
}
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// API để tạo terms mới (cho các ngôn ngữ khác)
|
||||
exports.create = async (req, res) => {
|
||||
try {
|
||||
const { hero, page, content, language } = req.body;
|
||||
|
||||
if (!language) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Language is required"
|
||||
});
|
||||
}
|
||||
|
||||
// Kiểm tra đã tồn tại chưa
|
||||
const existing = await Terms.findOne({ name: "default", language: language });
|
||||
if (existing) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Terms already exists for language: " + language
|
||||
});
|
||||
}
|
||||
|
||||
// Parse JSON nếu cần
|
||||
const parseJson = (data) => {
|
||||
if (!data) return null;
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error("JSON parse error:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const terms = new Terms({
|
||||
name: "default",
|
||||
language: language,
|
||||
hero: parseJson(hero) || {},
|
||||
page: parseJson(page) || {},
|
||||
content: parseJson(content) || {},
|
||||
version: "2.0.0",
|
||||
isActive: true,
|
||||
migratedFromOldStructure: false
|
||||
});
|
||||
|
||||
await terms.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Terms created successfully for language: " + language,
|
||||
data: terms
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error creating terms:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Error creating terms"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API để xóa terms (theo ngôn ngữ)
|
||||
exports.delete = async (req, res) => {
|
||||
try {
|
||||
const language = req.params.lang;
|
||||
|
||||
if (!language) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Language is required"
|
||||
});
|
||||
}
|
||||
|
||||
// Không cho phép xóa tiếng Anh mặc định
|
||||
if (language === "en") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Cannot delete default English terms"
|
||||
});
|
||||
}
|
||||
|
||||
const result = await Terms.deleteOne({ name: "default", language: language });
|
||||
|
||||
if (result.deletedCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Terms not found for language: " + language
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Terms deleted successfully for language: " + language
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error deleting terms:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Error deleting terms"
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user