forked from UKSOURCE/cms.lams
Merge branch 'develop' of https://gits.techvanguard.vn/UKSOURCE/cms.lams into feat/duy-20042026-HomepageAbout
This commit is contained in:
+88
-151
@@ -33,29 +33,23 @@ exports.index = async (req, res) => {
|
||||
// Prepare data for view
|
||||
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 || "",
|
||||
signInButton: header.signInButton || {
|
||||
label: "Sign In",
|
||||
href: "/signin",
|
||||
},
|
||||
ctaButton: header.ctaButton || {
|
||||
label: "Request Info",
|
||||
href: "/request",
|
||||
},
|
||||
}
|
||||
: {
|
||||
topbar: {
|
||||
contactInfo: {
|
||||
phone: "",
|
||||
email: "",
|
||||
location: "",
|
||||
},
|
||||
socialLinks: [],
|
||||
},
|
||||
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
|
||||
// switches tabs client-side
|
||||
@@ -123,14 +117,12 @@ exports.show = async (req, res) => {
|
||||
// Admin: Create header
|
||||
exports.store = async (req, res) => {
|
||||
try {
|
||||
const { top, offcanvas, menu, logo, ctaButton, status, order } = req.body;
|
||||
const { logo, signInButton, ctaButton, status, order } = req.body;
|
||||
|
||||
const header = new Header({
|
||||
top,
|
||||
offcanvas,
|
||||
menu,
|
||||
logo,
|
||||
ctaButton,
|
||||
logo: logo ? { light: logo } : {},
|
||||
signInButton: signInButton || { label: "Sign In", href: "/signin" },
|
||||
ctaButton: ctaButton || {},
|
||||
status: status || "active",
|
||||
order: order || 1,
|
||||
});
|
||||
@@ -152,129 +144,81 @@ exports.store = async (req, res) => {
|
||||
// Admin: Update header
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
let { top, topbarJson, offcanvas, menu, logo, ctaButton, status, order } =
|
||||
req.body;
|
||||
const { logo, signInButton, ctaButton, status, order } = req.body;
|
||||
|
||||
console.log("=== UPDATE REQUEST RECEIVED ===");
|
||||
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ó
|
||||
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 || [],
|
||||
};
|
||||
// Upsert logic: find existing header or prepare to create new one
|
||||
let header = await Header.findOne().sort({ order: 1 });
|
||||
let headerId = header?._id;
|
||||
|
||||
if (logo) {
|
||||
updateData.logo = logoData;
|
||||
}
|
||||
// Capture BEFORE state for audit logging
|
||||
const beforeData = header
|
||||
? JSON.parse(JSON.stringify(header.toObject()))
|
||||
: {};
|
||||
|
||||
console.log(
|
||||
"Preparing to update header with data:",
|
||||
JSON.stringify(updateData, null, 2),
|
||||
);
|
||||
if (!header) {
|
||||
console.log("No existing header found, creating new one");
|
||||
// Create new header document
|
||||
header = new Header({
|
||||
logo: logo ? { light: logo } : {},
|
||||
signInButton: signInButton || { label: "Sign In", href: "/signin" },
|
||||
ctaButton: ctaButton || {},
|
||||
status: status || "active",
|
||||
order: order || 1,
|
||||
});
|
||||
await header.save();
|
||||
console.log("✓ Header created:", header._id);
|
||||
|
||||
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,
|
||||
// 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({
|
||||
success: true,
|
||||
message: "Header created successfully",
|
||||
data: header,
|
||||
});
|
||||
}
|
||||
|
||||
// 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 });
|
||||
if (!header) {
|
||||
console.log("No existing header found, creating new one");
|
||||
// Tạo header mới nếu chưa có
|
||||
header = new Header({
|
||||
top,
|
||||
offcanvas,
|
||||
menu,
|
||||
logo: logo ? { light: logo } : {},
|
||||
ctaButton,
|
||||
status: status || "active",
|
||||
order: order || 1,
|
||||
});
|
||||
await header.save();
|
||||
console.log("✓ Header created:", header._id);
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "Header created successfully",
|
||||
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ũ
|
||||
let logoData = {};
|
||||
// Prepare logo data - merge with existing data
|
||||
let logoData = header.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 = {
|
||||
light: logo,
|
||||
dark: existingHeader?.logo?.dark || "",
|
||||
alt: existingHeader?.logo?.alt || "",
|
||||
dark: header.logo?.dark || "",
|
||||
alt: header.logo?.alt || "",
|
||||
};
|
||||
}
|
||||
|
||||
// Prepare update data
|
||||
const updateData = {
|
||||
top,
|
||||
offcanvas,
|
||||
menu,
|
||||
ctaButton,
|
||||
status,
|
||||
order,
|
||||
logo: logoData,
|
||||
signInButton: signInButton || header.signInButton,
|
||||
ctaButton: ctaButton || header.ctaButton,
|
||||
};
|
||||
|
||||
if (logo) {
|
||||
updateData.logo = logoData;
|
||||
}
|
||||
if (status !== undefined) updateData.status = status;
|
||||
if (order !== undefined) updateData.order = order;
|
||||
|
||||
console.log(
|
||||
"Preparing to update header with data:",
|
||||
JSON.stringify(updateData, null, 2),
|
||||
);
|
||||
|
||||
// ✅ Capture BEFORE state
|
||||
const beforeHeader = await Header.findById(headerId);
|
||||
const beforeData = beforeHeader
|
||||
? JSON.parse(JSON.stringify(beforeHeader.toObject()))
|
||||
: {};
|
||||
|
||||
// Update existing header
|
||||
const updatedHeader = await Header.findByIdAndUpdate(headerId, updateData, {
|
||||
new: 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()));
|
||||
|
||||
// ✅ AUDIT LOGGING - Header Updated
|
||||
// Audit logging - Header Updated
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
if (changes.length > 0) {
|
||||
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) => {
|
||||
try {
|
||||
const header = await Header.findOne({ status: "active" }).sort({
|
||||
const header = await Header.findOne().sort({
|
||||
order: 1,
|
||||
});
|
||||
|
||||
if (!header) {
|
||||
return res.status(404).json({
|
||||
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({
|
||||
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) {
|
||||
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,
|
||||
url: item.url,
|
||||
type: item.type,
|
||||
status: item.status || 'active',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -196,7 +197,7 @@ exports.reorder = async (req, res) => {
|
||||
// Public API: Get active menu as clean tree
|
||||
exports.api = async (req, res) => {
|
||||
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);
|
||||
res.json({ success: true, data: tree });
|
||||
} catch (error) {
|
||||
|
||||
+12
-77
@@ -1,84 +1,7 @@
|
||||
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(
|
||||
{
|
||||
// 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: {
|
||||
light: String,
|
||||
@@ -86,6 +9,18 @@ const headerSchema = new mongoose.Schema(
|
||||
alt: String,
|
||||
},
|
||||
|
||||
// Sign In Button
|
||||
signInButton: {
|
||||
label: {
|
||||
type: String,
|
||||
default: "Sign In",
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
default: "/signin",
|
||||
},
|
||||
},
|
||||
|
||||
// CTA Button
|
||||
ctaButton: {
|
||||
label: String,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
@@ -43,9 +43,6 @@ router.get("/api/about", aboutController.api);
|
||||
// Header API route
|
||||
router.get("/api/header", headerController.api);
|
||||
|
||||
// Menu Tree API route (for frontend)
|
||||
router.get("/api/menu-tree", headerController.getMenuTreeAPI);
|
||||
|
||||
// Header Menu New Module 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="content-with-fixed-buttons">
|
||||
<!-- Hidden inputs for JSON data -->
|
||||
<input type="hidden" name="topbarJson" id="topbarJson" />
|
||||
<input type="hidden" name="logo" id="logoInput" />
|
||||
<input type="hidden" name="activeTab" id="activeTabInput" value="topbar" />
|
||||
<input type="hidden" name="menuUpdates" id="menuUpdates" />
|
||||
<input type="hidden" name="activeTab" id="activeTabInput" value="logo" />
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<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">
|
||||
<a
|
||||
class="nav-link <%= activeTab === 'logo' ? 'active' : '' %>"
|
||||
@@ -41,6 +29,16 @@
|
||||
<i class="fas fa-image me-2"></i>Logo
|
||||
</a>
|
||||
</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">
|
||||
<a
|
||||
class="nav-link <%= activeTab === 'menu' ? 'active' : '' %>"
|
||||
@@ -56,79 +54,6 @@
|
||||
|
||||
<div class="card-body">
|
||||
<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 -->
|
||||
<div class="tab-pane fade <%= activeTab === 'logo' ? 'show active' : '' %>" id="logo" role="tabpanel">
|
||||
<div class="row g-4">
|
||||
@@ -179,6 +104,52 @@
|
||||
</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 -->
|
||||
<div class="tab-pane fade <%= activeTab === 'menu' ? 'show active' : '' %>" id="menu" role="tabpanel">
|
||||
<%- include('menu') %>
|
||||
@@ -202,12 +173,6 @@
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
let socialLinkIndex = 0;
|
||||
|
||||
// Initialize social links from data
|
||||
initializeSocialLinks();
|
||||
updateHiddenInputs();
|
||||
|
||||
// Safely remove any lingering modal backdrops on page load/navigation
|
||||
function cleanupModals() {
|
||||
// Basic reset
|
||||
@@ -291,7 +256,7 @@
|
||||
}
|
||||
|
||||
// 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 => {
|
||||
input.addEventListener('input', markChanged);
|
||||
input.addEventListener('change', markChanged);
|
||||
@@ -338,7 +303,6 @@
|
||||
if (saveHeaderBtn) {
|
||||
saveHeaderBtn.addEventListener('click', async function (e) {
|
||||
console.log('=== TRACE: saveHeaderBtn Clicked (Unified) ===');
|
||||
updateHiddenInputs();
|
||||
|
||||
const submitBtn = this;
|
||||
const originalText = submitBtn.innerHTML;
|
||||
@@ -346,11 +310,17 @@
|
||||
submitBtn.disabled = true;
|
||||
|
||||
try {
|
||||
// 1. Collect and Save Topbar & Logo
|
||||
// 1. Collect and Save Header Data (Logo + Buttons)
|
||||
const headerData = {
|
||||
topbarJson: document.getElementById('topbarJson').value,
|
||||
logo: document.getElementById('logoInput').value,
|
||||
activeTab: document.getElementById('activeTabInput').value
|
||||
logo: document.getElementById('logoImage').value,
|
||||
signInButton: {
|
||||
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', {
|
||||
@@ -382,7 +352,7 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('=== TRACE: Unified Save ERROR ===', error);
|
||||
showNotification('Lỗi: ' + error.message, 'error');
|
||||
showNotification('Error: ' + error.message, 'error');
|
||||
} finally {
|
||||
submitBtn.innerHTML = originalText;
|
||||
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
|
||||
@@ -990,41 +550,6 @@
|
||||
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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user