feat(contact-button): add floating contact widget admin management

Add CMS support for floating contact widget with Facebook/Zalo quick
actions. Includes mongoose schema, admin UI tab, image upload with
sharp resize presets, deferred form submission with draft persistence,
and upload middleware error handling.
This commit is contained in:
Tống Thành Đạt
2026-04-07 19:36:20 +07:00
parent e86e5d2c46
commit ffe2f12bb3
12 changed files with 1328 additions and 52 deletions

View File

@@ -0,0 +1,115 @@
require("dotenv").config();
const connectDB = require("../config/database");
const DEFAULT_FACEBOOK_URL = "https://www.facebook.com/hailearning.edu.vn/";
const DEFAULT_PANEL_TITLE = "Anh chị cần tư vấn hay hỗ trợ gì thêm không ạ?";
const DEFAULT_BRAND_IMAGE = "/assets/img/logo/black-logo.svg";
const DEFAULT_FACEBOOK_ICON = "/uploads/home/floating-contact/Facebook_Logo_Primary.webp";
const DEFAULT_ZALO_ICON = "/uploads/home/floating-contact/Icon_of_Zalo.svg.webp";
function normalizePhoneForZalo(value = "") {
const digits = String(value).replace(/\D/g, "");
if (!digits) {
return "";
}
if (digits.startsWith("84")) {
return digits;
}
if (digits.startsWith("0")) {
return `84${digits.slice(1)}`;
}
return digits;
}
async function migrate() {
const mongoose = require("mongoose");
let ownConn = false;
try {
const wasConnected = mongoose.connection.readyState === 1;
await connectDB();
if (!wasConnected) {
ownConn = true;
}
const Home = require("../models/home");
const Footer = require("../models/footer");
const footer = await Footer.findOne().sort({ updatedAt: -1 }).lean();
const footerPhone = footer?.top?.phone?.href || footer?.top?.phone?.display || "";
const zaloPhone = normalizePhoneForZalo(footerPhone) || "84961834040";
const zaloUrl = `https://zalo.me/${zaloPhone}`;
const defaultFloatingContact = {
enabled: true,
position: "bottom-right",
panelTitle: DEFAULT_PANEL_TITLE,
brand: {
imageSrc: DEFAULT_BRAND_IMAGE,
imageAlt: "HAI Learning",
},
trigger: {
icon: "fa-comments",
},
actions: [
{
id: "facebook",
platform: "facebook",
enabled: true,
label: "Nhắn tin qua Facebook",
subtitle: "facebook.com/hailearning.edu.vn",
href: DEFAULT_FACEBOOK_URL,
iconImage: DEFAULT_FACEBOOK_ICON,
iconType: "iconClass",
iconClass: "fa-brands fa-facebook-messenger",
iconText: "",
order: 1,
},
{
id: "zalo",
platform: "zalo",
enabled: true,
label: "Nhắn tin qua Zalo",
subtitle: `zalo.me/${zaloPhone}`,
href: zaloUrl,
iconImage: DEFAULT_ZALO_ICON,
iconType: "iconText",
iconClass: "",
iconText: "Zalo",
order: 2,
},
],
};
const updateResult = await Home.updateMany(
{ floatingContact: { $exists: false } },
{ $set: { floatingContact: defaultFloatingContact } },
);
if (updateResult.matchedCount === 0) {
await Home.create({ floatingContact: defaultFloatingContact });
console.log("Created a Home document with default floatingContact data.");
} else {
console.log(
`Updated ${updateResult.modifiedCount} Home document(s) with floatingContact defaults.`,
);
}
if (ownConn && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
}
process.exit(0);
} catch (error) {
console.error("Failed to add floatingContact to Home:", error);
if (ownConn && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
}
process.exit(1);
}
}
migrate();