forked from UKSOURCE/cms.hailearning.edu.vn
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:
@@ -1,7 +1,98 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const sharp = require('sharp');
|
||||
const jsonHelper = require('../utils/jsonHelper');
|
||||
|
||||
const imageResizePresets = {
|
||||
floatingContactBrandImage: { width: 104, height: 104, quality: 88 },
|
||||
floatingContactTriggerIcon: { width: 96, height: 96, quality: 88 },
|
||||
floatingContactActionIcon: { width: 84, height: 84, quality: 88 },
|
||||
};
|
||||
|
||||
const isSvgFile = (filePath) => path.extname(filePath).toLowerCase() === '.svg';
|
||||
|
||||
function scheduleTemporaryFileCleanup(filePath, attemptsLeft = 5, delayMs = 250) {
|
||||
if (!filePath || attemptsLeft <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
scheduleTemporaryFileCleanup(filePath, attemptsLeft - 1, delayMs * 2);
|
||||
}
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
function getFinalUploadTarget(file, req, forceWebp = false) {
|
||||
const parsedPath = path.parse(file.path);
|
||||
const requestedFileName =
|
||||
req.uploadFinalFileName ||
|
||||
file.filename.replace('.__upload__', '');
|
||||
const parsedRequestedFileName = path.parse(requestedFileName);
|
||||
const finalFileName = forceWebp
|
||||
? `${parsedRequestedFileName.name}.webp`
|
||||
: requestedFileName;
|
||||
const finalDirectory = req.uploadFinalDirectory || parsedPath.dir;
|
||||
|
||||
return {
|
||||
finalFileName,
|
||||
finalPath: path.join(finalDirectory, finalFileName),
|
||||
};
|
||||
}
|
||||
|
||||
async function finalizeUploadedImage(file, req, resizePreset) {
|
||||
const preset = imageResizePresets[resizePreset];
|
||||
|
||||
if (!file) {
|
||||
return file;
|
||||
}
|
||||
|
||||
if (!preset || isSvgFile(file.path)) {
|
||||
const { finalFileName, finalPath } = getFinalUploadTarget(file, req);
|
||||
|
||||
if (path.resolve(file.path) !== path.resolve(finalPath)) {
|
||||
fs.renameSync(file.path, finalPath);
|
||||
}
|
||||
|
||||
return {
|
||||
...file,
|
||||
filename: finalFileName,
|
||||
path: finalPath,
|
||||
};
|
||||
}
|
||||
|
||||
const { finalFileName, finalPath } = getFinalUploadTarget(file, req, true);
|
||||
|
||||
await sharp(file.path)
|
||||
.resize(preset.width, preset.height, {
|
||||
fit: 'contain',
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.webp({ quality: preset.quality })
|
||||
.toFile(finalPath);
|
||||
|
||||
if (fs.existsSync(file.path)) {
|
||||
try {
|
||||
fs.unlinkSync(file.path);
|
||||
} catch (cleanupError) {
|
||||
console.warn('Unable to remove original uploaded image after optimization:', cleanupError.message);
|
||||
scheduleTemporaryFileCleanup(file.path);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...file,
|
||||
filename: finalFileName,
|
||||
path: finalPath,
|
||||
mimetype: 'image/webp',
|
||||
};
|
||||
}
|
||||
|
||||
// Controller xử lý upload ảnh
|
||||
const uploadController = {
|
||||
// Upload ảnh và trả về đường dẫn
|
||||
@@ -13,15 +104,14 @@ const uploadController = {
|
||||
|
||||
// Lấy loại ảnh từ query params
|
||||
const imageType = req.query.imageType || 'general';
|
||||
const resizePreset = req.query.resizePreset || '';
|
||||
req.file = await finalizeUploadedImage(req.file, req, resizePreset);
|
||||
|
||||
// Tạo đường dẫn tương đối để lưu vào database
|
||||
const relativePath = `/uploads/${imageType}/${req.file.filename}`;
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const fullUrl = `${baseUrl}${relativePath}`;
|
||||
|
||||
// Kiểm tra nếu file đã tồn tại từ trước
|
||||
const fileAlreadyExists = req.fileAlreadyExists || false;
|
||||
|
||||
// Nếu client yêu cầu cập nhật trực tiếp file JSON (ví dụ activities.json),
|
||||
// thì đồng bộ camps.image và camps.camp-detail.hero.bgImage
|
||||
try {
|
||||
@@ -60,8 +150,9 @@ const uploadController = {
|
||||
success: true,
|
||||
path: relativePath,
|
||||
url: fullUrl,
|
||||
reused: fileAlreadyExists,
|
||||
message: fileAlreadyExists ? 'Using existing file' : 'File uploaded successfully'
|
||||
resizePreset: resizePreset || null,
|
||||
reused: false,
|
||||
message: 'File uploaded successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error uploading image:', error);
|
||||
@@ -225,4 +316,4 @@ const uploadController = {
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = uploadController;
|
||||
module.exports = uploadController;
|
||||
|
||||
Reference in New Issue
Block a user