forked from UKSOURCE/cms.lams
Merge pull request 'header' (#4) from feat/toan-22042026-header into develop
Reviewed-on: UKSOURCE/cms.lams#4
This commit is contained in:
+78
-141
@@ -33,29 +33,23 @@ exports.index = async (req, res) => {
|
|||||||
// Prepare data for view
|
// Prepare data for view
|
||||||
const data = header
|
const data = header
|
||||||
? {
|
? {
|
||||||
topbar: {
|
|
||||||
contactInfo: {
|
|
||||||
phone: header.top?.phone || "",
|
|
||||||
email: header.top?.email || "",
|
|
||||||
location: header.top?.location || "",
|
|
||||||
},
|
|
||||||
socialLinks: header.top?.socialLinks || [],
|
|
||||||
},
|
|
||||||
logo: header.logo?.light || "",
|
logo: header.logo?.light || "",
|
||||||
|
signInButton: header.signInButton || {
|
||||||
|
label: "Sign In",
|
||||||
|
href: "/signin",
|
||||||
|
},
|
||||||
|
ctaButton: header.ctaButton || {
|
||||||
|
label: "Request Info",
|
||||||
|
href: "/request",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
topbar: {
|
|
||||||
contactInfo: {
|
|
||||||
phone: "",
|
|
||||||
email: "",
|
|
||||||
location: "",
|
|
||||||
},
|
|
||||||
socialLinks: [],
|
|
||||||
},
|
|
||||||
logo: "",
|
logo: "",
|
||||||
|
signInButton: { label: "Sign In", href: "/signin" },
|
||||||
|
ctaButton: { label: "Request Info", href: "/request" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeTab = req.query.tab || "topbar";
|
const activeTab = req.query.tab || "logo";
|
||||||
|
|
||||||
// Always fetch menu items to ensure they are available even if the user
|
// Always fetch menu items to ensure they are available even if the user
|
||||||
// switches tabs client-side
|
// switches tabs client-side
|
||||||
@@ -123,14 +117,12 @@ exports.show = async (req, res) => {
|
|||||||
// Admin: Create header
|
// Admin: Create header
|
||||||
exports.store = async (req, res) => {
|
exports.store = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { top, offcanvas, menu, logo, ctaButton, status, order } = req.body;
|
const { logo, signInButton, ctaButton, status, order } = req.body;
|
||||||
|
|
||||||
const header = new Header({
|
const header = new Header({
|
||||||
top,
|
logo: logo ? { light: logo } : {},
|
||||||
offcanvas,
|
signInButton: signInButton || { label: "Sign In", href: "/signin" },
|
||||||
menu,
|
ctaButton: ctaButton || {},
|
||||||
logo,
|
|
||||||
ctaButton,
|
|
||||||
status: status || "active",
|
status: status || "active",
|
||||||
order: order || 1,
|
order: order || 1,
|
||||||
});
|
});
|
||||||
@@ -152,129 +144,81 @@ exports.store = async (req, res) => {
|
|||||||
// Admin: Update header
|
// Admin: Update header
|
||||||
exports.update = async (req, res) => {
|
exports.update = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
let { top, topbarJson, offcanvas, menu, logo, ctaButton, status, order } =
|
const { logo, signInButton, ctaButton, status, order } = req.body;
|
||||||
req.body;
|
|
||||||
|
|
||||||
console.log("=== UPDATE REQUEST RECEIVED ===");
|
console.log("=== UPDATE REQUEST RECEIVED ===");
|
||||||
console.log("Raw body:", JSON.stringify(req.body, null, 2));
|
console.log("Raw body:", JSON.stringify(req.body, null, 2));
|
||||||
console.log("topbarJson type:", typeof topbarJson);
|
|
||||||
console.log("topbarJson value:", topbarJson);
|
|
||||||
|
|
||||||
// Nếu có topbarJson, parse nó
|
// Upsert logic: find existing header or prepare to create new one
|
||||||
if (topbarJson && typeof topbarJson === "string") {
|
|
||||||
try {
|
|
||||||
const parsedData = JSON.parse(topbarJson);
|
|
||||||
console.log("✓ Parsed topbarJson successfully:", parsedData);
|
|
||||||
// Chuyển đổi từ topbarData sang top format
|
|
||||||
top = {
|
|
||||||
phone: parsedData.contactInfo?.phone || "",
|
|
||||||
email: parsedData.contactInfo?.email || "",
|
|
||||||
location: parsedData.contactInfo?.location || "",
|
|
||||||
socialLinks: parsedData.socialLinks || [],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (logo) {
|
|
||||||
updateData.logo = logoData;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"Preparing to update header with data:",
|
|
||||||
JSON.stringify(updateData, null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedHeader = await Header.findByIdAndUpdate(
|
|
||||||
headerId,
|
|
||||||
updateData,
|
|
||||||
{ new: true, runValidators: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!updatedHeader) {
|
|
||||||
console.error("✗ Header not found with ID:", headerId);
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
message: "Header not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: "Header updated successfully",
|
|
||||||
data: updatedHeader,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("✗ Error updating header:", error);
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nếu không có id, tìm header đầu tiên hoặc tạo mới
|
|
||||||
let headerId = req.params.id;
|
|
||||||
|
|
||||||
if (!headerId) {
|
|
||||||
// Tìm header đầu tiên
|
|
||||||
let header = await Header.findOne().sort({ order: 1 });
|
let header = await Header.findOne().sort({ order: 1 });
|
||||||
|
let headerId = header?._id;
|
||||||
|
|
||||||
|
// Capture BEFORE state for audit logging
|
||||||
|
const beforeData = header
|
||||||
|
? JSON.parse(JSON.stringify(header.toObject()))
|
||||||
|
: {};
|
||||||
|
|
||||||
if (!header) {
|
if (!header) {
|
||||||
console.log("No existing header found, creating new one");
|
console.log("No existing header found, creating new one");
|
||||||
// Tạo header mới nếu chưa có
|
// Create new header document
|
||||||
header = new Header({
|
header = new Header({
|
||||||
top,
|
|
||||||
offcanvas,
|
|
||||||
menu,
|
|
||||||
logo: logo ? { light: logo } : {},
|
logo: logo ? { light: logo } : {},
|
||||||
ctaButton,
|
signInButton: signInButton || { label: "Sign In", href: "/signin" },
|
||||||
|
ctaButton: ctaButton || {},
|
||||||
status: status || "active",
|
status: status || "active",
|
||||||
order: order || 1,
|
order: order || 1,
|
||||||
});
|
});
|
||||||
await header.save();
|
await header.save();
|
||||||
console.log("✓ Header created:", header._id);
|
console.log("✓ Header created:", header._id);
|
||||||
|
|
||||||
|
// Audit log for creation
|
||||||
|
const afterData = JSON.parse(JSON.stringify(header.toObject()));
|
||||||
|
const changes = diffObject(beforeData, afterData);
|
||||||
|
if (changes.length > 0) {
|
||||||
|
await writeAuditLog({
|
||||||
|
model: "Header",
|
||||||
|
documentId: header._id,
|
||||||
|
action: AUDIT_ACTIONS.UPDATE_HEADER,
|
||||||
|
before: beforeData,
|
||||||
|
after: afterData,
|
||||||
|
changes,
|
||||||
|
req,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Header created successfully",
|
message: "Header created successfully",
|
||||||
data: header,
|
data: header,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
headerId = header._id;
|
|
||||||
console.log("✓ Found existing header:", headerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chuẩn bị dữ liệu logo - merge với dữ liệu cũ
|
// Prepare logo data - merge with existing data
|
||||||
let logoData = {};
|
let logoData = header.logo || {};
|
||||||
if (logo) {
|
if (logo) {
|
||||||
// Nếu có logo mới, lấy dữ liệu cũ và update light
|
|
||||||
const existingHeader = await Header.findById(headerId);
|
|
||||||
logoData = {
|
logoData = {
|
||||||
light: logo,
|
light: logo,
|
||||||
dark: existingHeader?.logo?.dark || "",
|
dark: header.logo?.dark || "",
|
||||||
alt: existingHeader?.logo?.alt || "",
|
alt: header.logo?.alt || "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare update data
|
||||||
const updateData = {
|
const updateData = {
|
||||||
top,
|
logo: logoData,
|
||||||
offcanvas,
|
signInButton: signInButton || header.signInButton,
|
||||||
menu,
|
ctaButton: ctaButton || header.ctaButton,
|
||||||
ctaButton,
|
|
||||||
status,
|
|
||||||
order,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (logo) {
|
if (status !== undefined) updateData.status = status;
|
||||||
updateData.logo = logoData;
|
if (order !== undefined) updateData.order = order;
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"Preparing to update header with data:",
|
"Preparing to update header with data:",
|
||||||
JSON.stringify(updateData, null, 2),
|
JSON.stringify(updateData, null, 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ✅ Capture BEFORE state
|
// Update existing header
|
||||||
const beforeHeader = await Header.findById(headerId);
|
|
||||||
const beforeData = beforeHeader
|
|
||||||
? JSON.parse(JSON.stringify(beforeHeader.toObject()))
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const updatedHeader = await Header.findByIdAndUpdate(headerId, updateData, {
|
const updatedHeader = await Header.findByIdAndUpdate(headerId, updateData, {
|
||||||
new: true,
|
new: true,
|
||||||
runValidators: true,
|
runValidators: true,
|
||||||
@@ -288,10 +232,10 @@ exports.update = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Capture AFTER state
|
// Capture AFTER state for audit logging
|
||||||
const afterData = JSON.parse(JSON.stringify(updatedHeader.toObject()));
|
const afterData = JSON.parse(JSON.stringify(updatedHeader.toObject()));
|
||||||
|
|
||||||
// ✅ AUDIT LOGGING - Header Updated
|
// Audit logging - Header Updated
|
||||||
const changes = diffObject(beforeData, afterData);
|
const changes = diffObject(beforeData, afterData);
|
||||||
if (changes.length > 0) {
|
if (changes.length > 0) {
|
||||||
await writeAuditLog({
|
await writeAuditLog({
|
||||||
@@ -384,23 +328,41 @@ exports.destroy = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Public API: Get active header
|
// Public API: Get header (returns header regardless of status)
|
||||||
exports.api = async (req, res) => {
|
exports.api = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const header = await Header.findOne({ status: "active" }).sort({
|
const header = await Header.findOne().sort({
|
||||||
order: 1,
|
order: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!header) {
|
if (!header) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "No active header found",
|
message: "No header found",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const baseUrl = (process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`).replace(/\/$/, '');
|
||||||
|
const rawLogoPath = header.logo?.light || '';
|
||||||
|
const logoImage = rawLogoPath.startsWith('http') ? rawLogoPath : `${baseUrl}${rawLogoPath}`;
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: header,
|
data: {
|
||||||
|
logo: {
|
||||||
|
image: logoImage,
|
||||||
|
href: "/",
|
||||||
|
},
|
||||||
|
signInButton: {
|
||||||
|
label: header.signInButton?.label || "Sign In",
|
||||||
|
href: header.signInButton?.href || "/signin",
|
||||||
|
},
|
||||||
|
ctaButton: {
|
||||||
|
label: header.ctaButton?.label || "Request Info",
|
||||||
|
href: header.ctaButton?.href || "/request",
|
||||||
|
},
|
||||||
|
status: header.status || "active",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
@@ -410,28 +372,3 @@ exports.api = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Public API: Get menu tree structure
|
|
||||||
exports.getMenuTreeAPI = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const header = await Header.findOne({ status: "active" }).sort({
|
|
||||||
order: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!header || !header.menu) {
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
message: "No active menu found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: header.menu,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const buildMenuTree = (items, parentId = null, isPublic = false) => {
|
|||||||
title: item.title,
|
title: item.title,
|
||||||
url: item.url,
|
url: item.url,
|
||||||
type: item.type,
|
type: item.type,
|
||||||
|
status: item.status || 'active',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +197,7 @@ exports.reorder = async (req, res) => {
|
|||||||
// Public API: Get active menu as clean tree
|
// Public API: Get active menu as clean tree
|
||||||
exports.api = async (req, res) => {
|
exports.api = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const items = await HeaderMenu.find({ status: "active" }).sort({ order: 1 });
|
const items = await HeaderMenu.find().sort({ order: 1 });
|
||||||
const tree = buildMenuTree(items, null, true);
|
const tree = buildMenuTree(items, null, true);
|
||||||
res.json({ success: true, data: tree });
|
res.json({ success: true, data: tree });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
+12
-77
@@ -1,84 +1,7 @@
|
|||||||
const mongoose = require("mongoose");
|
const mongoose = require("mongoose");
|
||||||
|
|
||||||
const socialLinkSchema = new mongoose.Schema(
|
|
||||||
{
|
|
||||||
platform: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
enum: ["linkedin", "twitter", "instagram", "youtube", "facebook"],
|
|
||||||
},
|
|
||||||
url: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
icon: String,
|
|
||||||
order: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ _id: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
const languageSchema = new mongoose.Schema(
|
|
||||||
{
|
|
||||||
name: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ _id: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
const menuItemSchema = new mongoose.Schema(
|
|
||||||
{
|
|
||||||
label: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
href: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
icon: String,
|
|
||||||
order: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
children: [this],
|
|
||||||
},
|
|
||||||
{ _id: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
const headerSchema = new mongoose.Schema(
|
const headerSchema = new mongoose.Schema(
|
||||||
{
|
{
|
||||||
// Top bar
|
|
||||||
top: {
|
|
||||||
phone: String,
|
|
||||||
email: String,
|
|
||||||
location: String,
|
|
||||||
socialLinks: [socialLinkSchema],
|
|
||||||
languages: [languageSchema],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Offcanvas
|
|
||||||
offcanvas: {
|
|
||||||
description: String,
|
|
||||||
contactInfo: {
|
|
||||||
address: String,
|
|
||||||
email: String,
|
|
||||||
workingHours: String,
|
|
||||||
phone: String,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Menu
|
|
||||||
menu: [menuItemSchema],
|
|
||||||
|
|
||||||
// Logo
|
// Logo
|
||||||
logo: {
|
logo: {
|
||||||
light: String,
|
light: String,
|
||||||
@@ -86,6 +9,18 @@ const headerSchema = new mongoose.Schema(
|
|||||||
alt: String,
|
alt: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Sign In Button
|
||||||
|
signInButton: {
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: "Sign In",
|
||||||
|
},
|
||||||
|
href: {
|
||||||
|
type: String,
|
||||||
|
default: "/signin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// CTA Button
|
// CTA Button
|
||||||
ctaButton: {
|
ctaButton: {
|
||||||
label: String,
|
label: String,
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
@@ -50,9 +50,6 @@ router.get("/api/about-us", aboutUsController.getAbout);
|
|||||||
// Header API route
|
// Header API route
|
||||||
router.get("/api/header", headerController.api);
|
router.get("/api/header", headerController.api);
|
||||||
|
|
||||||
// Menu Tree API route (for frontend)
|
|
||||||
router.get("/api/menu-tree", headerController.getMenuTreeAPI);
|
|
||||||
|
|
||||||
// Header Menu New Module API
|
// Header Menu New Module API
|
||||||
router.get("/api/header-menu", headerMenuController.api);
|
router.get("/api/header-menu", headerMenuController.api);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const slugify = require('slugify');
|
||||||
|
|
||||||
|
// Data from lams/app/components/layout/Header/header.json
|
||||||
|
const headerJsonData = {
|
||||||
|
"logo": {
|
||||||
|
"image": "/uploads/header/logo.jp",
|
||||||
|
"href": "/"
|
||||||
|
},
|
||||||
|
"navLinks": [
|
||||||
|
{
|
||||||
|
"label": "About Us",
|
||||||
|
"href": "/about",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "History & Milestones",
|
||||||
|
"href": "/about/history"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Accreditation",
|
||||||
|
"href": "/about/accreditation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Partnerships",
|
||||||
|
"href": "/about/partnerships"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Programs",
|
||||||
|
"href": "/programmes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Student Support",
|
||||||
|
"href": "/student-support"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Admissions & Tuition",
|
||||||
|
"href": "/admissions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Blog",
|
||||||
|
"href": "/blog"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Contact",
|
||||||
|
"href": "/contact"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"actions": {
|
||||||
|
"signIn": {
|
||||||
|
"label": "Sign In",
|
||||||
|
"href": "/signin"
|
||||||
|
},
|
||||||
|
"cta": {
|
||||||
|
"label": "Request Info",
|
||||||
|
"href": "/request"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function seedHeader() {
|
||||||
|
try {
|
||||||
|
console.log('=== Seed Header Data from JSON ===');
|
||||||
|
console.log('Connecting to MongoDB...');
|
||||||
|
|
||||||
|
await mongoose.connect(process.env.MONGODB_URI);
|
||||||
|
console.log('✓ Connected to MongoDB');
|
||||||
|
|
||||||
|
const Header = require('../models/header');
|
||||||
|
const HeaderMenu = require('../models/headerMenu');
|
||||||
|
|
||||||
|
// Step 1: Seed Header document
|
||||||
|
console.log('\nStep 1: Seeding Header document...');
|
||||||
|
|
||||||
|
const existingHeader = await Header.findOne();
|
||||||
|
|
||||||
|
if (existingHeader) {
|
||||||
|
console.log('⚠ Header document already exists. Updating...');
|
||||||
|
existingHeader.logo = {
|
||||||
|
light: headerJsonData.logo.image,
|
||||||
|
dark: '',
|
||||||
|
alt: 'LAMS Logo',
|
||||||
|
};
|
||||||
|
existingHeader.signInButton = {
|
||||||
|
label: headerJsonData.actions.signIn.label,
|
||||||
|
href: headerJsonData.actions.signIn.href,
|
||||||
|
};
|
||||||
|
existingHeader.ctaButton = {
|
||||||
|
label: headerJsonData.actions.cta.label,
|
||||||
|
href: headerJsonData.actions.cta.href,
|
||||||
|
style: 'primary',
|
||||||
|
};
|
||||||
|
existingHeader.status = 'active';
|
||||||
|
await existingHeader.save();
|
||||||
|
console.log('✓ Header document updated');
|
||||||
|
} else {
|
||||||
|
const header = new Header({
|
||||||
|
logo: {
|
||||||
|
light: headerJsonData.logo.image,
|
||||||
|
dark: '',
|
||||||
|
alt: 'LAMS Logo',
|
||||||
|
},
|
||||||
|
signInButton: {
|
||||||
|
label: headerJsonData.actions.signIn.label,
|
||||||
|
href: headerJsonData.actions.signIn.href,
|
||||||
|
},
|
||||||
|
ctaButton: {
|
||||||
|
label: headerJsonData.actions.cta.label,
|
||||||
|
href: headerJsonData.actions.cta.href,
|
||||||
|
style: 'primary',
|
||||||
|
},
|
||||||
|
status: 'active',
|
||||||
|
order: 1,
|
||||||
|
});
|
||||||
|
await header.save();
|
||||||
|
console.log('✓ Header document created');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Seed HeaderMenu documents
|
||||||
|
console.log('\nStep 2: Seeding HeaderMenu documents...');
|
||||||
|
|
||||||
|
const existingMenuCount = await HeaderMenu.countDocuments();
|
||||||
|
|
||||||
|
if (existingMenuCount > 0) {
|
||||||
|
console.log(`⚠ Found ${existingMenuCount} existing menu items. Skipping menu seed.`);
|
||||||
|
console.log(' To re-seed menu, delete existing menu items first.');
|
||||||
|
} else {
|
||||||
|
let order = 0;
|
||||||
|
|
||||||
|
for (const navLink of headerJsonData.navLinks) {
|
||||||
|
// Create parent menu item
|
||||||
|
const parentSlug = slugify(navLink.label, { lower: true, strict: true });
|
||||||
|
const parentMenu = new HeaderMenu({
|
||||||
|
title: navLink.label,
|
||||||
|
slug: parentSlug,
|
||||||
|
url: navLink.href,
|
||||||
|
parentId: null,
|
||||||
|
order: order++,
|
||||||
|
status: 'active',
|
||||||
|
type: 'internal',
|
||||||
|
});
|
||||||
|
await parentMenu.save();
|
||||||
|
console.log(` ✓ Created parent menu: ${navLink.label}`);
|
||||||
|
|
||||||
|
// Create children menu items if they exist
|
||||||
|
if (navLink.children && navLink.children.length > 0) {
|
||||||
|
let childOrder = 0;
|
||||||
|
for (const child of navLink.children) {
|
||||||
|
const childSlug = slugify(child.label, { lower: true, strict: true });
|
||||||
|
const childMenu = new HeaderMenu({
|
||||||
|
title: child.label,
|
||||||
|
slug: childSlug,
|
||||||
|
url: child.href,
|
||||||
|
parentId: parentMenu._id,
|
||||||
|
order: childOrder++,
|
||||||
|
status: 'active',
|
||||||
|
type: 'internal',
|
||||||
|
});
|
||||||
|
await childMenu.save();
|
||||||
|
console.log(` ✓ Created child menu: ${child.label}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ Created ${order} parent menu items with their children`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n=== Seed Complete ===');
|
||||||
|
await mongoose.disconnect();
|
||||||
|
console.log('✓ Disconnected from MongoDB');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('✗ Seed failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run seed if this script is executed directly
|
||||||
|
if (require.main === module) {
|
||||||
|
seedHeader()
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = seedHeader;
|
||||||
+69
-544
@@ -12,25 +12,13 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="content-with-fixed-buttons">
|
<div class="content-with-fixed-buttons">
|
||||||
<!-- Hidden inputs for JSON data -->
|
<!-- Hidden inputs for JSON data -->
|
||||||
<input type="hidden" name="topbarJson" id="topbarJson" />
|
|
||||||
<input type="hidden" name="logo" id="logoInput" />
|
<input type="hidden" name="logo" id="logoInput" />
|
||||||
<input type="hidden" name="activeTab" id="activeTabInput" value="topbar" />
|
<input type="hidden" name="activeTab" id="activeTabInput" value="logo" />
|
||||||
<input type="hidden" name="menuUpdates" id="menuUpdates" />
|
|
||||||
|
|
||||||
<!-- Navigation Tabs -->
|
<!-- Navigation Tabs -->
|
||||||
<div class="card shadow-sm border-0 mb-4">
|
<div class="card shadow-sm border-0 mb-4">
|
||||||
<div class="card-header bg-white border-bottom">
|
<div class="card-header bg-white border-bottom">
|
||||||
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
||||||
<li class="nav-item">
|
|
||||||
<a
|
|
||||||
class="nav-link <%= activeTab === 'topbar' ? 'active' : '' %>"
|
|
||||||
data-bs-toggle="tab"
|
|
||||||
href="#topbar"
|
|
||||||
role="tab"
|
|
||||||
>
|
|
||||||
<i class="fas fa-bars me-2"></i>Topbar
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a
|
||||||
class="nav-link <%= activeTab === 'logo' ? 'active' : '' %>"
|
class="nav-link <%= activeTab === 'logo' ? 'active' : '' %>"
|
||||||
@@ -41,6 +29,16 @@
|
|||||||
<i class="fas fa-image me-2"></i>Logo
|
<i class="fas fa-image me-2"></i>Logo
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
class="nav-link <%= activeTab === 'buttons' ? 'active' : '' %>"
|
||||||
|
data-bs-toggle="tab"
|
||||||
|
href="#buttons"
|
||||||
|
role="tab"
|
||||||
|
>
|
||||||
|
<i class="fas fa-mouse-pointer me-2"></i>Buttons
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a
|
||||||
class="nav-link <%= activeTab === 'menu' ? 'active' : '' %>"
|
class="nav-link <%= activeTab === 'menu' ? 'active' : '' %>"
|
||||||
@@ -56,79 +54,6 @@
|
|||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<!-- Topbar Tab -->
|
|
||||||
<div class="tab-pane fade <%= activeTab === 'topbar' ? 'show active' : '' %>" id="topbar" role="tabpanel">
|
|
||||||
<div class="row g-4">
|
|
||||||
<!-- Contact Information -->
|
|
||||||
<div class="col-md-12">
|
|
||||||
<div class="card border shadow-sm">
|
|
||||||
<div class="card-header bg-white">
|
|
||||||
<h6 class="mb-0">
|
|
||||||
<i class="fas fa-phone me-2"></i>Contact Information
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label fw-medium">Phone Number</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
id="contactPhone"
|
|
||||||
value="<%= data.topbar.contactInfo.phone %>"
|
|
||||||
placeholder="+1 234 567 890"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label fw-medium">Email Address</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
class="form-control"
|
|
||||||
id="contactEmail"
|
|
||||||
value="<%= data.topbar.contactInfo.email || '' %>"
|
|
||||||
placeholder="info@example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label fw-medium">Location</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
id="contactLocation"
|
|
||||||
value="<%= data.topbar.contactInfo.location || '' %>"
|
|
||||||
placeholder="69 Street, 5th Avenue LA, United States"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Social Links -->
|
|
||||||
<div class="col-md-12">
|
|
||||||
<div class="card border shadow-sm">
|
|
||||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
||||||
<h6 class="mb-0">
|
|
||||||
<i class="fas fa-share-alt me-2"></i>Social Media Links
|
|
||||||
</h6>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-primary btn-sm"
|
|
||||||
id="addSocialLink"
|
|
||||||
>
|
|
||||||
<i class="fas fa-plus me-1"></i>Add Social Link
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div id="socialLinksContainer" class="social-links-sortable">
|
|
||||||
<!-- Social links will be populated here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Logo Tab -->
|
<!-- Logo Tab -->
|
||||||
<div class="tab-pane fade <%= activeTab === 'logo' ? 'show active' : '' %>" id="logo" role="tabpanel">
|
<div class="tab-pane fade <%= activeTab === 'logo' ? 'show active' : '' %>" id="logo" role="tabpanel">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
@@ -179,6 +104,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Buttons Tab -->
|
||||||
|
<div class="tab-pane fade <%= activeTab === 'buttons' ? 'show active' : '' %>" id="buttons" role="tabpanel">
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Sign In Button -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card border shadow-sm">
|
||||||
|
<div class="card-header bg-white">
|
||||||
|
<h6 class="mb-0"><i class="fas fa-sign-in-alt me-2"></i>Sign In Button</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-medium">Label</label>
|
||||||
|
<input type="text" class="form-control" id="signInLabel"
|
||||||
|
value="<%= data.signInButton.label %>" placeholder="Sign In" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-medium">URL</label>
|
||||||
|
<input type="text" class="form-control" id="signInHref"
|
||||||
|
value="<%= data.signInButton.href %>" placeholder="/signin" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- CTA Button -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card border shadow-sm">
|
||||||
|
<div class="card-header bg-white">
|
||||||
|
<h6 class="mb-0"><i class="fas fa-mouse-pointer me-2"></i>CTA Button</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-medium">Label</label>
|
||||||
|
<input type="text" class="form-control" id="ctaLabel"
|
||||||
|
value="<%= data.ctaButton.label %>" placeholder="Request Info" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-medium">URL</label>
|
||||||
|
<input type="text" class="form-control" id="ctaHref"
|
||||||
|
value="<%= data.ctaButton.href %>" placeholder="/request" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Menu Structure Tab -->
|
<!-- Menu Structure Tab -->
|
||||||
<div class="tab-pane fade <%= activeTab === 'menu' ? 'show active' : '' %>" id="menu" role="tabpanel">
|
<div class="tab-pane fade <%= activeTab === 'menu' ? 'show active' : '' %>" id="menu" role="tabpanel">
|
||||||
<%- include('menu') %>
|
<%- include('menu') %>
|
||||||
@@ -202,12 +173,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
let socialLinkIndex = 0;
|
|
||||||
|
|
||||||
// Initialize social links from data
|
|
||||||
initializeSocialLinks();
|
|
||||||
updateHiddenInputs();
|
|
||||||
|
|
||||||
// Safely remove any lingering modal backdrops on page load/navigation
|
// Safely remove any lingering modal backdrops on page load/navigation
|
||||||
function cleanupModals() {
|
function cleanupModals() {
|
||||||
// Basic reset
|
// Basic reset
|
||||||
@@ -291,7 +256,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attach listeners to all inputs for change detection
|
// Attach listeners to all inputs for change detection
|
||||||
const headerInputs = document.querySelectorAll('#topbar input, #logo input');
|
const headerInputs = document.querySelectorAll('#logo input, #buttons input');
|
||||||
headerInputs.forEach(input => {
|
headerInputs.forEach(input => {
|
||||||
input.addEventListener('input', markChanged);
|
input.addEventListener('input', markChanged);
|
||||||
input.addEventListener('change', markChanged);
|
input.addEventListener('change', markChanged);
|
||||||
@@ -338,7 +303,6 @@
|
|||||||
if (saveHeaderBtn) {
|
if (saveHeaderBtn) {
|
||||||
saveHeaderBtn.addEventListener('click', async function (e) {
|
saveHeaderBtn.addEventListener('click', async function (e) {
|
||||||
console.log('=== TRACE: saveHeaderBtn Clicked (Unified) ===');
|
console.log('=== TRACE: saveHeaderBtn Clicked (Unified) ===');
|
||||||
updateHiddenInputs();
|
|
||||||
|
|
||||||
const submitBtn = this;
|
const submitBtn = this;
|
||||||
const originalText = submitBtn.innerHTML;
|
const originalText = submitBtn.innerHTML;
|
||||||
@@ -346,11 +310,17 @@
|
|||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Collect and Save Topbar & Logo
|
// 1. Collect and Save Header Data (Logo + Buttons)
|
||||||
const headerData = {
|
const headerData = {
|
||||||
topbarJson: document.getElementById('topbarJson').value,
|
logo: document.getElementById('logoImage').value,
|
||||||
logo: document.getElementById('logoInput').value,
|
signInButton: {
|
||||||
activeTab: document.getElementById('activeTabInput').value
|
label: document.getElementById('signInLabel').value,
|
||||||
|
href: document.getElementById('signInHref').value,
|
||||||
|
},
|
||||||
|
ctaButton: {
|
||||||
|
label: document.getElementById('ctaLabel').value,
|
||||||
|
href: document.getElementById('ctaHref').value,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerResponse = await fetch('/admin/header/update', {
|
const headerResponse = await fetch('/admin/header/update', {
|
||||||
@@ -382,7 +352,7 @@
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('=== TRACE: Unified Save ERROR ===', error);
|
console.error('=== TRACE: Unified Save ERROR ===', error);
|
||||||
showNotification('Lỗi: ' + error.message, 'error');
|
showNotification('Error: ' + error.message, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
submitBtn.innerHTML = originalText;
|
submitBtn.innerHTML = originalText;
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
@@ -390,416 +360,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateHiddenInputs() {
|
|
||||||
const topbarData = {
|
|
||||||
contactInfo: {
|
|
||||||
phone: document.getElementById('contactPhone').value || '',
|
|
||||||
email: document.getElementById('contactEmail').value || '',
|
|
||||||
location: document.getElementById('contactLocation').value || ''
|
|
||||||
},
|
|
||||||
socialLinks: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Collect social links
|
|
||||||
document.querySelectorAll('.social-platform').forEach((input) => {
|
|
||||||
const platform = input.value.trim();
|
|
||||||
const urlInput = document.querySelector(`.social-url[data-index="${input.dataset.index}"]`);
|
|
||||||
const iconInput = document.querySelector(`.social-icon[data-index="${input.dataset.index}"]`);
|
|
||||||
|
|
||||||
const url = urlInput ? urlInput.value.trim() : '';
|
|
||||||
const icon = iconInput ? iconInput.value.trim() : '';
|
|
||||||
|
|
||||||
if (platform && url) {
|
|
||||||
topbarData.socialLinks.push({
|
|
||||||
platform: platform,
|
|
||||||
url: url,
|
|
||||||
icon: icon
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('topbarJson').value = JSON.stringify(topbarData);
|
|
||||||
document.getElementById('logoInput').value = document.getElementById('logoImage').value || '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const menuUpdates = collectMenuUpdates();
|
|
||||||
document.getElementById('menuUpdates').value = JSON.stringify(menuUpdates);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error collecting menu updates:', e);
|
|
||||||
document.getElementById('menuUpdates').value = '[]';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collect menu updates from the menu tree
|
|
||||||
* Returns an empty array if no menu tree exists
|
|
||||||
*/
|
|
||||||
function collectMenuUpdates() {
|
|
||||||
// For now, return empty array as menu structure management is coming soon
|
|
||||||
// This function can be expanded when menu tree functionality is implemented
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeSocialLinks() {
|
|
||||||
const container = document.getElementById('socialLinksContainer');
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
// Extract social links safely from EJS data
|
|
||||||
let socialLinks = [];
|
|
||||||
try {
|
|
||||||
socialLinks = <%- JSON.stringify(data.topbar.socialLinks || []) %>;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error parsing social links data:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socialLinks.length === 0) {
|
|
||||||
// Add default platforms if no social links exist
|
|
||||||
const platforms = ['linkedin', 'twitter', 'instagram', 'youtube'];
|
|
||||||
platforms.forEach((platform, index) => {
|
|
||||||
addSocialLinkRow(platform, '', `fa-brands fa-${platform}`, index);
|
|
||||||
});
|
|
||||||
socialLinkIndex = platforms.length;
|
|
||||||
} else {
|
|
||||||
// Load existing social links
|
|
||||||
socialLinks.forEach((social, index) => {
|
|
||||||
const platform = social.platform || '';
|
|
||||||
const url = social.url || '';
|
|
||||||
const icon = social.icon || '';
|
|
||||||
addSocialLinkRow(platform, url, icon, index);
|
|
||||||
});
|
|
||||||
socialLinkIndex = socialLinks.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSocialLinkRow(platform, url, icon, index) {
|
|
||||||
const container = document.getElementById('socialLinksContainer');
|
|
||||||
const socialLink = document.createElement('div');
|
|
||||||
socialLink.className = 'card mb-3 border social-link-item';
|
|
||||||
socialLink.dataset.platform = platform;
|
|
||||||
|
|
||||||
// Escape HTML to prevent XSS
|
|
||||||
const escapedPlatform = (platform || '').replace(/[&<>"']/g, char => ({
|
|
||||||
'&': '&',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'"': '"',
|
|
||||||
"'": '''
|
|
||||||
}[char]));
|
|
||||||
|
|
||||||
const escapedUrl = (url || '').replace(/[&<>"']/g, char => ({
|
|
||||||
'&': '&',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'"': '"',
|
|
||||||
"'": '''
|
|
||||||
}[char]));
|
|
||||||
|
|
||||||
const escapedIcon = (icon || '').replace(/[&<>"']/g, char => ({
|
|
||||||
'&': '&',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'"': '"',
|
|
||||||
"'": '''
|
|
||||||
}[char]));
|
|
||||||
|
|
||||||
socialLink.innerHTML = `
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row g-3 align-items-end">
|
|
||||||
<div class="col-md-1 d-flex justify-content-center align-items-center w-auto">
|
|
||||||
<label class="form-label fw-medium"> </label>
|
|
||||||
<div class="drag-handle" title="Drag to reorder" style="cursor: grab; font-size: 1.2rem; color: #999; user-select: none;">
|
|
||||||
<i class="fas fa-grip-vertical"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label fw-medium">Platform</label>
|
|
||||||
<input type="text" class="form-control social-platform" value="${escapedPlatform}" data-index="${index}" disabled />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label fw-medium">URL</label>
|
|
||||||
<input type="text" class="form-control social-url" value="${escapedUrl}" data-index="${index}" placeholder="https://..." />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label fw-medium">Icon Class</label>
|
|
||||||
<input type="text" class="form-control social-icon" value="${escapedIcon}" data-index="${index}" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label"> </label>
|
|
||||||
<div class="btn-group w-100" role="group">
|
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm edit-social-link mx-3 rounded" style="transform: none" data-index="${index}" title="Edit">
|
|
||||||
<i class="fas fa-edit"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-outline-danger btn-sm remove-social-link rounded" data-index="${index}" title="Delete">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
container.appendChild(socialLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
DRAG & DROP STYLING
|
|
||||||
============================================ */
|
|
||||||
const dragDropStyles = document.createElement('style');
|
|
||||||
dragDropStyles.textContent = `
|
|
||||||
.social-link-item {
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-handle {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
cursor: grab !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-handle:active {
|
|
||||||
cursor: grabbing !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-handle:hover {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
color: #0d6efd;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* SortableJS Classes */
|
|
||||||
.social-ghost {
|
|
||||||
opacity: 0.4;
|
|
||||||
border: 2px dashed #0d6efd !important;
|
|
||||||
background-color: #f8f9fa !important;
|
|
||||||
}
|
|
||||||
.social-chosen {
|
|
||||||
background-color: #eef3ff !important;
|
|
||||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1) !important;
|
|
||||||
}
|
|
||||||
.social-drag {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix Modal Freeze & Z-Index issues */
|
|
||||||
body.modal-open {
|
|
||||||
overflow: hidden !important;
|
|
||||||
padding-right: 0 !important;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(dragDropStyles);
|
|
||||||
|
|
||||||
document.addEventListener('click', function (e) {
|
|
||||||
if (e.target.closest('.remove-social-link')) {
|
|
||||||
e.preventDefault();
|
|
||||||
const btn = e.target.closest('.remove-social-link');
|
|
||||||
const index = btn.dataset.index;
|
|
||||||
const platformInput = document.querySelector(`.social-platform[data-index="${index}"]`);
|
|
||||||
const platform = platformInput.value;
|
|
||||||
|
|
||||||
if (confirm(`Delete ${platform} social link?`)) {
|
|
||||||
deleteSocialLink(platform, btn.closest('.card'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.target.closest('.edit-social-link')) {
|
|
||||||
e.preventDefault();
|
|
||||||
const btn = e.target.closest('.edit-social-link');
|
|
||||||
const index = btn.dataset.index;
|
|
||||||
const platformInput = document.querySelector(`.social-platform[data-index="${index}"]`);
|
|
||||||
const urlInput = document.querySelector(`.social-url[data-index="${index}"]`);
|
|
||||||
const iconInput = document.querySelector(`.social-icon[data-index="${index}"]`);
|
|
||||||
|
|
||||||
const platform = platformInput.value;
|
|
||||||
const url = urlInput.value;
|
|
||||||
const icon = iconInput.value;
|
|
||||||
|
|
||||||
showEditSocialLinkModal(platform, url, icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.target.closest('#addSocialLink')) {
|
|
||||||
e.preventDefault();
|
|
||||||
showAddSocialLinkModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function deleteSocialLink(platform, cardElement) {
|
|
||||||
if (confirm(`Delete ${platform} social link?`)) {
|
|
||||||
cardElement.remove();
|
|
||||||
updateHiddenInputs();
|
|
||||||
markChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showAddSocialLinkModal() {
|
|
||||||
const modal = document.createElement('div');
|
|
||||||
modal.className = 'modal fade';
|
|
||||||
modal.id = 'addSocialLinkModal';
|
|
||||||
modal.setAttribute('tabindex', '-1');
|
|
||||||
modal.innerHTML = `
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Add New Social Link</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label fw-medium">Platform <span class="text-danger">*</span></label>
|
|
||||||
<input type="text" class="form-control" id="newSocialPlatform" placeholder="e.g., linkedin, twitter, instagram, youtube, facebook" />
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label fw-medium">URL <span class="text-danger">*</span></label>
|
|
||||||
<input type="text" class="form-control" id="newSocialUrl" placeholder="https://..." />
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label fw-medium">Icon Class</label>
|
|
||||||
<input type="text" class="form-control" id="newSocialIcon" placeholder="e.g., fa-brands fa-linkedin" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
||||||
<button type="button" class="btn btn-primary" id="saveSocialLink">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.body.appendChild(modal);
|
|
||||||
const bsModal = new bootstrap.Modal(modal);
|
|
||||||
|
|
||||||
document.getElementById('saveSocialLink').addEventListener('click', function() {
|
|
||||||
const platform = document.getElementById('newSocialPlatform').value.trim();
|
|
||||||
const url = document.getElementById('newSocialUrl').value.trim();
|
|
||||||
const icon = document.getElementById('newSocialIcon').value.trim();
|
|
||||||
|
|
||||||
if (!platform) {
|
|
||||||
alert('Vui lòng nhập tên nền tảng');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
alert('Vui lòng nhập URL');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if platform already exists
|
|
||||||
const existingPlatforms = Array.from(document.querySelectorAll('.social-platform')).map(el => el.value);
|
|
||||||
if (existingPlatforms.includes(platform)) {
|
|
||||||
alert(`${platform} đã tồn tại`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
addSocialLinkViaAPI(platform, url, icon || `fa-brands fa-${platform}`, bsModal, modal);
|
|
||||||
});
|
|
||||||
|
|
||||||
bsModal.show();
|
|
||||||
|
|
||||||
// Focus on platform input
|
|
||||||
setTimeout(() => {
|
|
||||||
document.getElementById('newSocialPlatform').focus();
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showEditSocialLinkModal(platform, url, icon) {
|
|
||||||
const modal = document.createElement('div');
|
|
||||||
modal.className = 'modal fade';
|
|
||||||
modal.id = 'editSocialLinkModal';
|
|
||||||
modal.setAttribute('tabindex', '-1');
|
|
||||||
const platformDisplay = platform ? platform.charAt(0).toUpperCase() + platform.slice(1) : 'Social';
|
|
||||||
modal.innerHTML = `
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Edit ${platformDisplay} Link</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label fw-medium">Platform <span class="text-danger">*</span></label>
|
|
||||||
<input type="text" class="form-control" id="editSocialPlatform" value="${platform}" placeholder="e.g., linkedin, twitter, instagram" />
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label fw-medium">URL <span class="text-danger">*</span></label>
|
|
||||||
<input type="text" class="form-control" id="editSocialUrl" value="${url}" placeholder="https://www.example.com" />
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label fw-medium">Icon Class</label>
|
|
||||||
<input type="text" class="form-control" id="editSocialIcon" value="${icon}" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
||||||
<button type="button" class="btn btn-primary" id="updateSocialLink">Update</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.body.appendChild(modal);
|
|
||||||
const bsModal = new bootstrap.Modal(modal);
|
|
||||||
|
|
||||||
document.getElementById('updateSocialLink').addEventListener('click', function() {
|
|
||||||
const newPlatform = document.getElementById('editSocialPlatform').value.trim();
|
|
||||||
const newUrl = document.getElementById('editSocialUrl').value.trim();
|
|
||||||
const newIcon = document.getElementById('editSocialIcon').value.trim();
|
|
||||||
|
|
||||||
if (!newPlatform) {
|
|
||||||
alert('Vui lòng nhập tên nền tảng');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newUrl) {
|
|
||||||
alert('Vui lòng nhập URL');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSocialLinkViaAPIModal(platform, newPlatform, newUrl, newIcon, bsModal, modal);
|
|
||||||
});
|
|
||||||
|
|
||||||
bsModal.show();
|
|
||||||
|
|
||||||
// Focus on platform input
|
|
||||||
setTimeout(() => {
|
|
||||||
document.getElementById('editSocialPlatform').focus();
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSocialLinkViaAPI(platform, url, icon, modal, modalElement) {
|
|
||||||
addSocialLinkRow(platform, url, icon, Date.now());
|
|
||||||
updateHiddenInputs();
|
|
||||||
markChanged();
|
|
||||||
modal.hide();
|
|
||||||
modalElement.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSocialLinkViaAPIModal(oldPlatform, newPlatform, url, icon, modal, modalElement) {
|
|
||||||
newPlatform = newPlatform.toLowerCase().trim();
|
|
||||||
|
|
||||||
// Update the input fields in the DOM
|
|
||||||
const platformInputs = document.querySelectorAll(`.social-platform`);
|
|
||||||
const urlInputs = document.querySelectorAll(`.social-url`);
|
|
||||||
const iconInputs = document.querySelectorAll(`.social-icon`);
|
|
||||||
|
|
||||||
let found = false;
|
|
||||||
for (let i = 0; i < platformInputs.length; i++) {
|
|
||||||
if (platformInputs[i].value.toLowerCase() === oldPlatform.toLowerCase()) {
|
|
||||||
platformInputs[i].value = newPlatform;
|
|
||||||
urlInputs[i].value = url;
|
|
||||||
if (iconInputs[i]) iconInputs[i].value = icon;
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (found) {
|
|
||||||
modal.hide();
|
|
||||||
modalElement.remove();
|
|
||||||
updateHiddenInputs();
|
|
||||||
markChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show toast notification at top of page
|
* Show toast notification at top of page
|
||||||
@@ -990,41 +550,6 @@
|
|||||||
previewContainer.appendChild(img);
|
previewContainer.appendChild(img);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Sortable for Social Links
|
|
||||||
const socialLinksContainer = document.getElementById('socialLinksContainer');
|
|
||||||
const SortableLib = window.Sortable || Sortable;
|
|
||||||
|
|
||||||
console.log('=== TRACE: Social Sortable Init ===', {
|
|
||||||
containerExists: !!socialLinksContainer,
|
|
||||||
sortableDefined: typeof SortableLib !== 'undefined'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (socialLinksContainer && typeof SortableLib !== 'undefined') {
|
|
||||||
try {
|
|
||||||
new SortableLib(socialLinksContainer, {
|
|
||||||
animation: 150,
|
|
||||||
handle: '.drag-handle',
|
|
||||||
ghostClass: 'social-ghost',
|
|
||||||
chosenClass: 'social-chosen',
|
|
||||||
dragClass: 'social-drag',
|
|
||||||
forceFallback: true, // Use transition-based dragging for better compatibility
|
|
||||||
onStart: function() {
|
|
||||||
console.log('=== TRACE: Social Drag Started ===');
|
|
||||||
},
|
|
||||||
onEnd: function() {
|
|
||||||
console.log('=== TRACE: Social Drag Ended ===');
|
|
||||||
updateHiddenInputs();
|
|
||||||
markChanged();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log('=== TRACE: Sortable initialized for socialLinksContainer ===');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('=== TRACE: Sortable Init Error ===', err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('SortableJS not loaded or socialLinksContainer not found');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user