forked from UKSOURCE/cms.hailearning.edu.vn
Merge branch 'main' of https://gits.techvanguard.vn/UKSOURCE/cms.hailearning.edu.vn into hoanganh-03022026-appointment-contact-pricing
This commit is contained in:
BIN
.env.example
BIN
.env.example
Binary file not shown.
17
Dockerfile
17
Dockerfile
@@ -1,17 +0,0 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
|
||||
|
||||
WORKDIR /home/node/app
|
||||
|
||||
COPY --chown=node:node package*.json ./
|
||||
|
||||
USER node
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY --chown=node:node . .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD [ "npm", "start" ]
|
||||
@@ -35,7 +35,28 @@
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.bg-soft-success { background-color: var(--success-soft); color: var(--success-color); border: 1px solid rgba(40, 167, 69, 0.2); }
|
||||
.bg-soft-danger { background-color: var(--danger-soft); color: var(--danger-color); border: 1px solid rgba(220, 53, 69, 0.2); }
|
||||
.bg-soft-warning { background-color: var(--warning-soft); color: var(--warning-color); border: 1px solid rgba(255, 193, 7, 0.2); }
|
||||
.bg-soft-info { background-color: var(--info-soft); color: var(--info-color); border: 1px solid rgba(23, 162, 184, 0.2); }
|
||||
.bg-soft-success {
|
||||
background-color: var(--success-soft);
|
||||
color: var(--success-color);
|
||||
border: 1px solid rgba(40, 167, 69, 0.2);
|
||||
}
|
||||
.bg-soft-danger {
|
||||
background-color: var(--danger-soft);
|
||||
color: var(--danger-color);
|
||||
border: 1px solid rgba(220, 53, 69, 0.2);
|
||||
}
|
||||
.bg-soft-warning {
|
||||
background-color: var(--warning-soft);
|
||||
color: var(--warning-color);
|
||||
border: 1px solid rgba(255, 193, 7, 0.2);
|
||||
}
|
||||
.bg-soft-info {
|
||||
background-color: var(--info-soft);
|
||||
color: var(--info-color);
|
||||
border: 1px solid rgba(23, 162, 184, 0.2);
|
||||
}
|
||||
.bg-soft-secondary {
|
||||
background-color: #f8f9fa;
|
||||
color: #6c757d;
|
||||
border: 1px solid rgba(108, 117, 125, 0.2);
|
||||
}
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
const { addBaseUrlToImages } = require('../utils/imageHelper');
|
||||
const About = require('../models/about');
|
||||
|
||||
// Get about data from MongoDB
|
||||
const getAboutData = async () => {
|
||||
const about = await About.findOne().sort({ updatedAt: -1 });
|
||||
|
||||
// Trả về object rỗng với cấu trúc cơ bản nếu không có dữ liệu
|
||||
if (!about) {
|
||||
return {
|
||||
banner: {
|
||||
image: '',
|
||||
title: '',
|
||||
text: ''
|
||||
},
|
||||
about: {
|
||||
title: '',
|
||||
paragraphs: [],
|
||||
list_items: [],
|
||||
button: {
|
||||
text: '',
|
||||
url: ''
|
||||
},
|
||||
image: '',
|
||||
quote: {
|
||||
mark_image: '',
|
||||
title: '',
|
||||
text: '',
|
||||
author: ''
|
||||
}
|
||||
},
|
||||
values: {
|
||||
background_image: '',
|
||||
items: []
|
||||
},
|
||||
education: {
|
||||
images: {
|
||||
student1: '',
|
||||
student2: ''
|
||||
},
|
||||
subtitle: '',
|
||||
title: '',
|
||||
text: ''
|
||||
},
|
||||
advantages: {
|
||||
title: '',
|
||||
items: []
|
||||
},
|
||||
academic_board: {
|
||||
title: '',
|
||||
members: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return about;
|
||||
};
|
||||
|
||||
// Display about management page
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
const data = await getAboutData();
|
||||
res.render('admin/about', {
|
||||
title: 'About Management',
|
||||
data
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
req.flash('error_msg', 'Error loading about data');
|
||||
res.redirect('/admin/dashboard');
|
||||
}
|
||||
};
|
||||
|
||||
// Update about data
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
// Lấy document hiện tại từ MongoDB
|
||||
const currentData = await getAboutData();
|
||||
|
||||
// Danh sách các section cần cập nhật
|
||||
const sections = ['banner', 'about', 'values', 'education', 'advantages', 'academic_board'];
|
||||
const errors = [];
|
||||
let hasChanges = false;
|
||||
|
||||
// Tạo đối tượng dữ liệu mới dựa trên dữ liệu hiện tại
|
||||
const updatedData = { ...currentData.toObject() };
|
||||
|
||||
// Xử lý từng section
|
||||
sections.forEach(section => {
|
||||
try {
|
||||
// Kiểm tra nếu section không được gửi lên
|
||||
if (!req.body[section]) {
|
||||
console.warn(`No data for section: ${section}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse dữ liệu JSON từ form
|
||||
const newSectionData = JSON.parse(req.body[section]);
|
||||
|
||||
// So sánh dữ liệu mới với dữ liệu hiện tại
|
||||
const currentSectionData = currentData[section];
|
||||
const sectionHasChanges = JSON.stringify(newSectionData) !== JSON.stringify(currentSectionData);
|
||||
|
||||
// Nếu có thay đổi, cập nhật vào đối tượng dữ liệu mới
|
||||
if (sectionHasChanges) {
|
||||
updatedData[section] = newSectionData;
|
||||
hasChanges = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing section ${section}:`, error);
|
||||
errors.push(`Error processing ${section} data: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Nếu có lỗi, thông báo và chuyển hướng
|
||||
if (errors.length > 0) {
|
||||
req.flash('error_msg', `Data processing error: ${errors[0]}`);
|
||||
return req.session.save(() => res.redirect('/admin/about'));
|
||||
}
|
||||
|
||||
// Nếu không có thay đổi, thông báo và chuyển hướng
|
||||
if (!hasChanges) {
|
||||
req.flash('info_msg', 'No changes were made');
|
||||
return req.session.save(() => res.redirect('/admin/about'));
|
||||
}
|
||||
|
||||
try {
|
||||
// Cập nhật hoặc tạo mới document trong MongoDB
|
||||
if (currentData._id) {
|
||||
await About.findByIdAndUpdate(currentData._id, updatedData, { new: true });
|
||||
} else {
|
||||
await About.create(updatedData);
|
||||
}
|
||||
|
||||
// Success notification and redirect
|
||||
req.flash('success_msg', 'About data updated successfully');
|
||||
return req.session.save(() => res.redirect('/admin/about'));
|
||||
} catch (dbError) {
|
||||
console.error('Database error:', dbError);
|
||||
req.flash('error_msg', `Database error: ${dbError.message || 'Unknown'}`);
|
||||
return req.session.save(() => res.redirect('/admin/about'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Update error:', err);
|
||||
req.flash('error_msg', `Update error: ${err.message || 'Unknown'}`);
|
||||
return req.session.save(() => res.redirect('/admin/about'));
|
||||
}
|
||||
};
|
||||
|
||||
// API to get about data
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
const aboutData = await getAboutData();
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const processedData = addBaseUrlToImages(aboutData, baseUrl);
|
||||
res.json(processedData);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Error loading about data' });
|
||||
}
|
||||
};
|
||||
@@ -1,363 +1,141 @@
|
||||
const {addBaseUrlToImages} = require("../utils/imageHelper");
|
||||
const About = require("../models/about");
|
||||
const { addBaseUrlToImages } = require("../utils/imageHelper");
|
||||
const AboutUs = require("../models/aboutUs");
|
||||
const jsonHelper = require("../utils/jsonHelper");
|
||||
|
||||
// -------------------- Public (read-only) helpers --------------------
|
||||
// Map stored About document back to the original aboutUs.json shape
|
||||
function transformToAboutUs(doc) {
|
||||
if (!doc) return null;
|
||||
|
||||
const hero = {
|
||||
banner: doc.banner?.image || "",
|
||||
title: doc.banner?.title || "",
|
||||
breadcrumb: doc.banner?.text || "",
|
||||
};
|
||||
|
||||
const stats = Array.isArray(doc.advantages?.items)
|
||||
? doc.advantages.items.map((item) => ({
|
||||
number: item.number || "",
|
||||
description: item.title || "",
|
||||
}))
|
||||
: [];
|
||||
|
||||
const services = Array.isArray(doc.about?.paragraphs)
|
||||
? doc.about.paragraphs.map((p) => ({title: "", description: p}))
|
||||
: [];
|
||||
|
||||
const features = Array.isArray(doc.values?.items)
|
||||
? doc.values.items.map((i) => ({
|
||||
title: i.title || "",
|
||||
description: i.text || "",
|
||||
icon: i.icon || "",
|
||||
}))
|
||||
: [];
|
||||
|
||||
const events = Array.isArray(doc.academic_board?.members)
|
||||
? doc.academic_board.members.map((m) => ({
|
||||
imageUrl: m.image || "",
|
||||
date: "",
|
||||
title: m.title || "",
|
||||
description: "",
|
||||
authorName: m.name || "",
|
||||
authorRole: "",
|
||||
}))
|
||||
: [];
|
||||
|
||||
return {
|
||||
hero,
|
||||
stats,
|
||||
services,
|
||||
features,
|
||||
events,
|
||||
};
|
||||
}
|
||||
|
||||
// Get aboutUs data: prefer AboutUs collection, fallback to transforming About
|
||||
const getAboutUsData = async () => {
|
||||
// Prefer stored AboutUs document
|
||||
const aboutUsDoc = await AboutUs.findOne().sort({updatedAt: -1});
|
||||
if (aboutUsDoc)
|
||||
return aboutUsDoc.toObject ? aboutUsDoc.toObject() : aboutUsDoc;
|
||||
|
||||
// Fallback: transform legacy About document into aboutUs shape
|
||||
const about = await About.findOne().sort({updatedAt: -1});
|
||||
if (!about) return null;
|
||||
return transformToAboutUs(about);
|
||||
};
|
||||
|
||||
// -------------------- Admin (CRUD on AboutUs model) helpers --------------------
|
||||
// Default shape for AboutUs documents (matches data/aboutUs.json)
|
||||
const getDefaultAboutUsData = () => ({
|
||||
hero: {title: "", backgroundImage: ""},
|
||||
introduction: {
|
||||
subtitle: "",
|
||||
title: "",
|
||||
description: "",
|
||||
mainImage: "",
|
||||
services: [],
|
||||
},
|
||||
statistics: {
|
||||
items: [],
|
||||
},
|
||||
accommodation: {
|
||||
subtitle: "",
|
||||
title: "",
|
||||
description: "",
|
||||
features: [],
|
||||
},
|
||||
activities: {
|
||||
subtitle: "",
|
||||
title: "",
|
||||
description: "",
|
||||
gallery: [],
|
||||
},
|
||||
newsletter: {
|
||||
imagePath: "",
|
||||
title: "",
|
||||
description: "",
|
||||
buttonText: "",
|
||||
},
|
||||
events: {
|
||||
title: "",
|
||||
items: [],
|
||||
},
|
||||
});
|
||||
|
||||
// Get latest stored AboutUs document or default (returned as plain object)
|
||||
const getStoredAboutUs = async () => {
|
||||
const aboutUs = await AboutUs.findOne().sort({updatedAt: -1});
|
||||
if (!aboutUs) return getDefaultAboutUsData();
|
||||
return aboutUs.toObject ? aboutUs.toObject() : aboutUs;
|
||||
};
|
||||
|
||||
// -------------------- Public exports --------------------
|
||||
// Public endpoint: return AboutUs JSON (previously rendered HTML)
|
||||
exports.page = async (req, res) => {
|
||||
/**
|
||||
* GET /api/about
|
||||
* Lấy dữ liệu About Us (Public API cho website và CMS load dữ liệu)
|
||||
*/
|
||||
exports.getAbout = async (req, res) => {
|
||||
try {
|
||||
const aboutUsData = await getAboutUsData();
|
||||
// Force no-cache headers
|
||||
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||
res.setHeader('Pragma', 'no-cache');
|
||||
res.setHeader('Expires', '0');
|
||||
|
||||
const data = await AboutUs.getSingle();
|
||||
const rawData = data.toObject();
|
||||
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const processed = addBaseUrlToImages(aboutUsData || {}, baseUrl);
|
||||
return res.json(processed);
|
||||
} catch (err) {
|
||||
console.error("aboutUs.page error:", err);
|
||||
return res.status(500).json({ error: "Error loading about-us data" });
|
||||
const processedData = addBaseUrlToImages(rawData, baseUrl);
|
||||
|
||||
res.json(processedData);
|
||||
} catch (error) {
|
||||
console.error("Error getting about data:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Failed to get about data"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API endpoint to return aboutUs JSON
|
||||
exports.api = async (req, res) => {
|
||||
/**
|
||||
* PUT /api/about
|
||||
* Cập nhật dữ liệu About Us (Dùng cho AJAX từ CMS)
|
||||
*/
|
||||
exports.updateAbout = async (req, res) => {
|
||||
try {
|
||||
const aboutUsData = await getAboutUsData();
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const processed = addBaseUrlToImages(aboutUsData || {}, baseUrl);
|
||||
return res.json(processed);
|
||||
} catch (err) {
|
||||
console.error("aboutUs.api error:", err);
|
||||
return res.status(500).json({error: "Error loading about-us data"});
|
||||
let updateData = req.body;
|
||||
|
||||
// Nếu dữ liệu gửi qua trường aboutJson (dạng string JSON)
|
||||
if (updateData.aboutJson && typeof updateData.aboutJson === "string") {
|
||||
try {
|
||||
updateData = JSON.parse(updateData.aboutJson);
|
||||
} catch (e) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid JSON in aboutJson"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const doc = await AboutUs.getSingle();
|
||||
// Use .set() for better handling of nested objects/arrays in Mongoose
|
||||
doc.set(updateData);
|
||||
await doc.save();
|
||||
|
||||
// Fetch fresh data for syncing and returning
|
||||
const finalData = await AboutUs.findOne()
|
||||
.select('-_id -__v -createdAt -updatedAt')
|
||||
.lean();
|
||||
|
||||
// Update about.json file to keep it in sync
|
||||
jsonHelper.writeJsonFile("about", finalData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "About Us updated successfully",
|
||||
data: finalData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating about data:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Failed to update about data: " + error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API endpoint to return an array of AboutUs records (for frontend listing)
|
||||
exports.apiList = async (req, res) => {
|
||||
try {
|
||||
const docs = await AboutUs.find().sort({ updatedAt: -1 }).lean();
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const processed = (docs || []).map((d) => addBaseUrlToImages(d || {}, baseUrl));
|
||||
return res.json(processed);
|
||||
} catch (err) {
|
||||
console.error("aboutUs.apiList error:", err);
|
||||
return res.status(500).json({ error: "Error loading about-us list" });
|
||||
}
|
||||
};
|
||||
|
||||
// -------------------- Admin exports --------------------
|
||||
// Display AboutUs management page
|
||||
/**
|
||||
* Render admin page (Dùng cho Admin UI)
|
||||
*/
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
const data = await getStoredAboutUs();
|
||||
const items = await AboutUs.find().sort({updatedAt: -1}).limit(10);
|
||||
const data = await AboutUs.getSingle();
|
||||
const rawData = data.toObject();
|
||||
|
||||
const activeTab = req.query.activeTab || "hero";
|
||||
res.render("admin/aboutUs/index", {
|
||||
layout: "layouts/main",
|
||||
title: "About Us Management",
|
||||
data,
|
||||
items,
|
||||
frontendUrl:
|
||||
process.env.FRONTEND_URL || req.protocol + "://" + req.get("host"),
|
||||
currentPath: req.path,
|
||||
data: rawData,
|
||||
activeTab,
|
||||
user: req.session.user,
|
||||
currentPath: req.path,
|
||||
frontendUrl: process.env.FRONTEND_URL || 'http://localhost:3000'
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
req.flash("error_msg", "Error loading About Us data");
|
||||
console.error("Error in about index:", err);
|
||||
req.flash("error_msg", "Error loading About Us page");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
};
|
||||
|
||||
// Display create form
|
||||
exports.createForm = async (req, res) => {
|
||||
try {
|
||||
const data = getDefaultAboutUsData();
|
||||
|
||||
res.render("admin/aboutUs/create", {
|
||||
layout: "layouts/main",
|
||||
title: "Create About Us",
|
||||
data,
|
||||
currentPath: req.path,
|
||||
user: req.session.user,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
req.flash("error_msg", "Error loading create form");
|
||||
res.redirect("/admin/about-us");
|
||||
}
|
||||
};
|
||||
|
||||
// Create new AboutUs record
|
||||
exports.create = async (req, res) => {
|
||||
try {
|
||||
const aboutUsData = {
|
||||
hero: JSON.parse(req.body.hero || "{}"),
|
||||
introduction: JSON.parse(req.body.introduction || "{}"),
|
||||
statistics: JSON.parse(req.body.statistics || "{}"),
|
||||
accommodation: JSON.parse(req.body.accommodation || "{}"),
|
||||
activities: JSON.parse(req.body.activities || "{}"),
|
||||
newsletter: JSON.parse(req.body.newsletter || "{}"),
|
||||
events: JSON.parse(req.body.events || "{}"),
|
||||
};
|
||||
|
||||
const newAboutUs = new AboutUs(aboutUsData);
|
||||
await newAboutUs.save();
|
||||
|
||||
req.flash("success_msg", "About Us created successfully");
|
||||
res.redirect("/admin/about-us");
|
||||
} catch (err) {
|
||||
console.error("Create error:", err);
|
||||
req.flash("error_msg", `Create error: ${err.message || "Unknown"}`);
|
||||
res.redirect("/admin/about-us/create");
|
||||
}
|
||||
};
|
||||
|
||||
// Display edit form
|
||||
exports.editForm = async (req, res) => {
|
||||
try {
|
||||
const aboutUs = await AboutUs.findById(req.params.id);
|
||||
|
||||
if (!aboutUs) {
|
||||
req.flash("error_msg", "About Us record not found");
|
||||
return res.redirect("/admin/about-us");
|
||||
}
|
||||
|
||||
res.render("admin/aboutUs/edit", {
|
||||
layout: "layouts/main",
|
||||
title: "Edit About Us",
|
||||
data: aboutUs.toObject ? aboutUs.toObject() : aboutUs,
|
||||
currentPath: req.path,
|
||||
user: req.session.user,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
req.flash("error_msg", "Error loading edit form");
|
||||
res.redirect("/admin/about-us");
|
||||
}
|
||||
};
|
||||
|
||||
// Update AboutUs record
|
||||
/**
|
||||
* Update method cho form-based submission (Admin UI - Post fallback)
|
||||
*/
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
// Get current data
|
||||
const currentData = await getStoredAboutUs();
|
||||
|
||||
// Parse form data
|
||||
const sections = [
|
||||
"hero",
|
||||
"introduction",
|
||||
"statistics",
|
||||
"accommodation",
|
||||
"activities",
|
||||
"newsletter",
|
||||
"events",
|
||||
];
|
||||
const errors = [];
|
||||
let hasChanges = false;
|
||||
|
||||
// Create updated data object
|
||||
const updatedData = {
|
||||
...(currentData.toObject ? currentData.toObject() : currentData),
|
||||
};
|
||||
|
||||
// Process each section
|
||||
sections.forEach((section) => {
|
||||
let updateData = req.body;
|
||||
if (updateData.aboutJson && typeof updateData.aboutJson === "string") {
|
||||
try {
|
||||
if (!req.body[section]) {
|
||||
console.warn(`No data for section: ${section}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const newSectionData = JSON.parse(req.body[section]);
|
||||
const currentSectionData = currentData[section];
|
||||
const sectionHasChanges =
|
||||
JSON.stringify(newSectionData) !== JSON.stringify(currentSectionData);
|
||||
|
||||
if (sectionHasChanges) {
|
||||
updatedData[section] = newSectionData;
|
||||
hasChanges = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing section ${section}:`, error);
|
||||
errors.push(`Error processing ${section} data: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
req.flash("error_msg", `Data processing error: ${errors[0]}`);
|
||||
return req.session.save(() => res.redirect("/admin/about-us"));
|
||||
}
|
||||
|
||||
if (!hasChanges) {
|
||||
req.flash("info_msg", "No changes were made");
|
||||
return req.session.save(() => res.redirect("/admin/about-us"));
|
||||
}
|
||||
|
||||
try {
|
||||
// Only update existing document; do not create a new one here
|
||||
if (!currentData || !currentData._id) {
|
||||
req.flash("error_msg", "No existing About Us record to update. Create one first.");
|
||||
return req.session.save(() => res.redirect("/admin/about-us"));
|
||||
}
|
||||
|
||||
await AboutUs.findByIdAndUpdate(currentData._id, updatedData, {
|
||||
new: true,
|
||||
});
|
||||
|
||||
req.flash("success_msg", "About Us data updated successfully");
|
||||
return req.session.save(() => res.redirect("/admin/about-us"));
|
||||
} catch (dbError) {
|
||||
console.error("Database error:", dbError);
|
||||
req.flash("error_msg", `Database error: ${dbError.message || "Unknown"}`);
|
||||
return req.session.save(() => res.redirect("/admin/about-us"));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Update error:", err);
|
||||
req.flash("error_msg", `Update error: ${err.message || "Unknown"}`);
|
||||
return req.session.save(() => res.redirect("/admin/about-us"));
|
||||
}
|
||||
};
|
||||
|
||||
// Delete AboutUs record
|
||||
exports.delete = async (req, res) => {
|
||||
try {
|
||||
const aboutUs = await AboutUs.findById(req.params.id);
|
||||
|
||||
if (!aboutUs) {
|
||||
req.flash("error_msg", "About Us record not found");
|
||||
updateData = JSON.parse(updateData.aboutJson);
|
||||
} catch (e) {
|
||||
req.flash("error_msg", "Invalid JSON data");
|
||||
return res.redirect("/admin/about-us");
|
||||
}
|
||||
}
|
||||
|
||||
await AboutUs.findByIdAndDelete(req.params.id);
|
||||
const doc = await AboutUs.getSingle();
|
||||
doc.set(updateData);
|
||||
await doc.save();
|
||||
|
||||
req.flash("success_msg", "About Us record deleted successfully");
|
||||
res.redirect("/admin/about-us");
|
||||
const finalData = await AboutUs.findOne()
|
||||
.select('-_id -__v -createdAt -updatedAt')
|
||||
.lean();
|
||||
jsonHelper.writeJsonFile("about", finalData);
|
||||
|
||||
req.flash("success_msg", "About Us updated successfully");
|
||||
const activeTab = req.query.activeTab || "hero";
|
||||
res.redirect(`/admin/about-us?activeTab=${activeTab}`);
|
||||
} catch (err) {
|
||||
console.error("Delete error:", err);
|
||||
req.flash("error_msg", `Delete error: ${err.message || "Unknown"}`);
|
||||
console.error("Update error:", err);
|
||||
req.flash("error_msg", "Error updating About Us: " + err.message);
|
||||
res.redirect("/admin/about-us");
|
||||
}
|
||||
};
|
||||
|
||||
// Preview AboutUs record
|
||||
exports.preview = async (req, res) => {
|
||||
try {
|
||||
const aboutUs = await AboutUs.findById(req.params.id);
|
||||
|
||||
if (!aboutUs) {
|
||||
return res.status(404).json({error: "About Us record not found"});
|
||||
}
|
||||
|
||||
const processedData = addBaseUrlToImages(aboutUs.toObject());
|
||||
res.json(processedData);
|
||||
} catch (err) {
|
||||
console.error("Preview error:", err);
|
||||
res.status(500).json({error: "Error loading preview data"});
|
||||
}
|
||||
};
|
||||
// Aliases for compatibility
|
||||
exports.api = exports.getAbout;
|
||||
exports.page = exports.getAbout;
|
||||
exports.updateAboutUs = exports.updateAbout;
|
||||
|
||||
@@ -6,8 +6,8 @@ const slugify = require("slugify");
|
||||
*/
|
||||
const buildMenuTree = (items, parentId = null, isPublic = false) => {
|
||||
const branch = [];
|
||||
const children = items.filter(item =>
|
||||
String(item.parentId) === String(parentId) || (item.parentId === null && parentId === null)
|
||||
const children = items.filter(
|
||||
(item) => String(item.parentId) === String(parentId) || (item.parentId === null && parentId === null),
|
||||
);
|
||||
|
||||
for (const child of children) {
|
||||
@@ -20,7 +20,7 @@ const buildMenuTree = (items, parentId = null, isPublic = false) => {
|
||||
id: item._id,
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
type: item.type
|
||||
type: item.type,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ exports.index = async (req, res) => {
|
||||
// 2. Create Menu Item
|
||||
exports.store = async (req, res) => {
|
||||
try {
|
||||
console.log('=== BACKEND: store hit ===');
|
||||
console.log('Body:', req.body);
|
||||
console.log("=== BACKEND: store hit ===");
|
||||
console.log("Body:", req.body);
|
||||
const { title, url, parentId, order, status, type } = req.body;
|
||||
const slug = slugify(title, { lower: true, strict: true });
|
||||
|
||||
@@ -69,15 +69,27 @@ exports.store = async (req, res) => {
|
||||
parentId: parentId || null,
|
||||
order: order || 0,
|
||||
status: status || "active",
|
||||
type: type || "internal"
|
||||
type: type || "internal",
|
||||
});
|
||||
|
||||
const savedItem = await newItem.save();
|
||||
console.log('=== MENU CREATED ===', savedItem);
|
||||
console.log("=== MENU CREATED ===", savedItem);
|
||||
|
||||
// Return JSON for AJAX requests
|
||||
if (req.xhr || req.headers.accept?.indexOf("json") > -1) {
|
||||
return res.json({ success: true, message: "Menu item created successfully", data: savedItem });
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Menu item created successfully");
|
||||
res.redirect("/admin/header?tab=menu");
|
||||
} catch (error) {
|
||||
console.error('=== CREATE MENU ERROR ===', error);
|
||||
console.error("=== CREATE MENU ERROR ===", error);
|
||||
|
||||
// Return JSON for AJAX requests
|
||||
if (req.xhr || req.headers.accept?.indexOf("json") > -1) {
|
||||
return res.status(400).json({ success: false, message: error.message });
|
||||
}
|
||||
|
||||
req.flash("error_msg", "Failed to create menu item: " + error.message);
|
||||
res.redirect("/admin/header?tab=menu");
|
||||
}
|
||||
@@ -87,8 +99,8 @@ exports.store = async (req, res) => {
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
console.log('=== BACKEND: update hit ===', { id });
|
||||
console.log('Body:', req.body);
|
||||
console.log("=== BACKEND: update hit ===", { id });
|
||||
console.log("Body:", req.body);
|
||||
const { title, url, parentId, order, status, type } = req.body;
|
||||
|
||||
const updateData = {
|
||||
@@ -96,7 +108,7 @@ exports.update = async (req, res) => {
|
||||
parentId: parentId || null,
|
||||
order,
|
||||
status,
|
||||
type
|
||||
type,
|
||||
};
|
||||
|
||||
if (title) {
|
||||
@@ -107,15 +119,33 @@ exports.update = async (req, res) => {
|
||||
const updated = await HeaderMenu.findByIdAndUpdate(id, updateData, { new: true });
|
||||
|
||||
if (!updated) {
|
||||
console.log('=== UPDATE MENU NOT FOUND ===', id);
|
||||
console.log("=== UPDATE MENU NOT FOUND ===", id);
|
||||
|
||||
// Return JSON for AJAX requests
|
||||
if (req.xhr || req.headers.accept?.indexOf("json") > -1) {
|
||||
return res.status(404).json({ success: false, message: "Menu item not found" });
|
||||
}
|
||||
|
||||
req.flash("error_msg", "Menu item not found");
|
||||
} else {
|
||||
console.log('=== MENU UPDATED ===', updated);
|
||||
console.log("=== MENU UPDATED ===", updated);
|
||||
|
||||
// Return JSON for AJAX requests
|
||||
if (req.xhr || req.headers.accept?.indexOf("json") > -1) {
|
||||
return res.json({ success: true, message: "Menu item updated successfully", data: updated });
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Menu item updated successfully");
|
||||
}
|
||||
res.redirect("/admin/header?tab=menu");
|
||||
} catch (error) {
|
||||
console.error('=== UPDATE MENU ERROR ===', error);
|
||||
console.error("=== UPDATE MENU ERROR ===", error);
|
||||
|
||||
// Return JSON for AJAX requests
|
||||
if (req.xhr || req.headers.accept?.indexOf("json") > -1) {
|
||||
return res.status(400).json({ success: false, message: error.message });
|
||||
}
|
||||
|
||||
req.flash("error_msg", "Update failed: " + error.message);
|
||||
res.redirect("/admin/header?tab=menu");
|
||||
}
|
||||
@@ -126,16 +156,16 @@ exports.destroy = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.body;
|
||||
const menuId = id || req.params.id;
|
||||
console.log('=== BACKEND: destroy hit ===', { menuId, body: req.body });
|
||||
console.log("=== BACKEND: destroy hit ===", { menuId, body: req.body });
|
||||
|
||||
await deleteRecursive(menuId);
|
||||
await HeaderMenu.findByIdAndDelete(menuId);
|
||||
|
||||
console.log('=== MENU DELETED ===', menuId);
|
||||
console.log("=== MENU DELETED ===", menuId);
|
||||
req.flash("success_msg", "Menu item and its sub-menu deleted successfully");
|
||||
res.redirect("/admin/header?tab=menu");
|
||||
} catch (error) {
|
||||
console.error('=== DELETE MENU ERROR ===', error);
|
||||
console.error("=== DELETE MENU ERROR ===", error);
|
||||
req.flash("error_msg", "Delete failed: " + error.message);
|
||||
res.redirect("/admin/header?tab=menu");
|
||||
}
|
||||
@@ -147,11 +177,11 @@ exports.reorder = async (req, res) => {
|
||||
const { items } = req.body; // Array of { id, order, parentId }
|
||||
|
||||
if (items && Array.isArray(items)) {
|
||||
const bulkOps = items.map(item => ({
|
||||
const bulkOps = items.map((item) => ({
|
||||
updateOne: {
|
||||
filter: { _id: item.id },
|
||||
update: { order: item.order, parentId: item.parentId || null }
|
||||
}
|
||||
update: { order: item.order, parentId: item.parentId || null },
|
||||
},
|
||||
}));
|
||||
await HeaderMenu.bulkWrite(bulkOps);
|
||||
return res.json({ success: true, message: "Reordered successfully" });
|
||||
|
||||
@@ -7,8 +7,28 @@ const getHomeDoc = async () => Home.findOne().sort({ updatedAt: -1 });
|
||||
const getHomeData = async () => (await getHomeDoc())?.toObject() || {};
|
||||
|
||||
const getDefaultHomeData = () => ({
|
||||
hero: { title: "", subtitle: "", description: "", backgroundImage: "", videoUrl: "", primaryButton: {}, secondaryButton: {} },
|
||||
whyChooseUs: { heading: "", subheading: "", description: "", items: [], features: [], ctaButton: {} },
|
||||
hero: {
|
||||
backgroundImage: "",
|
||||
slides: [],
|
||||
title: "",
|
||||
subtitle: "",
|
||||
description: "",
|
||||
heroImage: "",
|
||||
videoUrl: "",
|
||||
primaryButton: {},
|
||||
secondaryButton: {},
|
||||
},
|
||||
whyChooseUs: {
|
||||
heading: "",
|
||||
subheading: "",
|
||||
description: "",
|
||||
highlightWord: "",
|
||||
mainImage: "",
|
||||
secondaryImage: "",
|
||||
items: [],
|
||||
features: [],
|
||||
ctaButton: {},
|
||||
},
|
||||
visaSolutions: { heading: "", subheading: "", items: [] },
|
||||
visaCountries: { heading: "", subheading: "", description: "", countries: [], ctaButton: {} },
|
||||
testimonials: { heading: "", subheading: "", videoUrl: "", videoThumbnail: "", items: [] },
|
||||
|
||||
123
data/about.json
Normal file
123
data/about.json
Normal file
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"hero": {
|
||||
"title": "About Us",
|
||||
"breadcrumb": [
|
||||
"Home",
|
||||
"About Us"
|
||||
],
|
||||
"backgroundImage": "/uploads/about/breadcrumb.jpg"
|
||||
},
|
||||
"intro": {
|
||||
"subheading": "Company Intro",
|
||||
"heading": "Building Pathways to Your Immigration Success",
|
||||
"description": "We provide expert guidance, personalized solutions, and transparent processes to help you achieve your immigration goals. Our dedicated team ensures a smooth journey, building pathways to your international success.",
|
||||
"image": "http://localhost:3001/uploads/about/intro.jpg"
|
||||
},
|
||||
"mission": {
|
||||
"subheading": "About Our Consultancy",
|
||||
"heading": "Turning Study Abroad Dreams Into Reality",
|
||||
"description": "We guide students with expert visa consulting, ensuring a smooth process from application to approval, turning study abroad aspirations into life-changing opportunities for a brighter future.",
|
||||
"images": {
|
||||
"main": "/assets/img/home-1/about/about-1.jpg",
|
||||
"secondary": "/assets/img/home-1/about/about-02.jpg",
|
||||
"bgShape": "/assets/img/home-1/about/Vector.png",
|
||||
"planeShape": "/assets/img/home-1/about/plane.png",
|
||||
"topShape": "/assets/img/home-1/about/shape.png",
|
||||
"globeShape": "/assets/img/home-1/about/globe.png"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"icon": "/assets/img/home-1/icon/01.svg",
|
||||
"label": "Global Reach",
|
||||
"description": "Expanding Opportunities Worldwide"
|
||||
},
|
||||
{
|
||||
"icon": "/assets/img/home-1/icon/01.svg",
|
||||
"label": "Global Reach",
|
||||
"description": "Expanding Opportunities Worldwide"
|
||||
}
|
||||
],
|
||||
"features": [
|
||||
"Fastest Visa form processing with skilled immigration agents",
|
||||
"Partnership with International Educational Institutions"
|
||||
],
|
||||
"ctaButton": {
|
||||
"label": "Get Started",
|
||||
"href": "/about"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"backgroundImage": "/assets/img/home-2/feature/bg-shape.png",
|
||||
"subheading": "Your Travel Made Easy",
|
||||
"heading": "Smooth Visa Journey Guaranteed",
|
||||
"description": "We provide expert guidance for every visa application, ensuring smooth processing, personalized support, and reliable assistance",
|
||||
"image": "/assets/img/home-2/feature/02.png",
|
||||
"items": [
|
||||
{
|
||||
"icon": "/assets/img/home-2/icon/01.png",
|
||||
"title": "Expert Consultants",
|
||||
"description": "Skilled and knowledgeable visa advisors. Skilled and knowledgeable visa advisors."
|
||||
},
|
||||
{
|
||||
"icon": "/assets/img/home-2/icon/01.png",
|
||||
"title": "Personalized Support",
|
||||
"description": "Skilled and knowledgeable visa advisors. Skilled and knowledgeable visa advisors."
|
||||
},
|
||||
{
|
||||
"icon": "/assets/img/home-2/icon/01.png",
|
||||
"title": "Transparent Process",
|
||||
"description": "Skilled and knowledgeable visa advisors. Skilled and knowledgeable visa advisors."
|
||||
}
|
||||
],
|
||||
"ctaButton": {
|
||||
"label": "Get Started Today",
|
||||
"href": "/contact"
|
||||
}
|
||||
},
|
||||
"news": {
|
||||
"subheading": "Visa Tips & Guides",
|
||||
"heading": "Latest Insights & Updates",
|
||||
"ctaButton": {
|
||||
"label": "view all articles",
|
||||
"href": "/blog"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"title": "Step-by-Step Guide to Applying for a Student Visa",
|
||||
"category": "Student Visa",
|
||||
"date": "20 August ,2025",
|
||||
"comments": 8,
|
||||
"author": {
|
||||
"name": "Sohel",
|
||||
"avatar": "/assets/img/home-1/news/client.png"
|
||||
},
|
||||
"link": "/blog/step-by-step-guide-student-visa",
|
||||
"thumbnail": "/assets/img/home-1/news/news-1.jpg"
|
||||
},
|
||||
{
|
||||
"title": "Tips to Prepare Financial Documents for Visa Approval",
|
||||
"category": "IELTS / TOEFL",
|
||||
"date": "20 August ,2025",
|
||||
"comments": 8,
|
||||
"author": {
|
||||
"name": "Sohel",
|
||||
"avatar": "/assets/img/home-1/news/client.png"
|
||||
},
|
||||
"link": "/blog/financial-documents-visa-approval",
|
||||
"thumbnail": "/assets/img/home-1/news/news-2.jpg"
|
||||
},
|
||||
{
|
||||
"title": "Post-Arrival Guide What Every Student Should Know",
|
||||
"category": "Study Abroad",
|
||||
"date": "20 August ,2025",
|
||||
"comments": 8,
|
||||
"author": {
|
||||
"name": "Sohel",
|
||||
"avatar": "/assets/img/home-1/news/client.png"
|
||||
},
|
||||
"link": "/blog/post-arrival-guide-students",
|
||||
"thumbnail": "/assets/img/home-1/news/news-3.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"avatars": [
|
||||
"yootheme/aboutImage/profile-face_1.jpg",
|
||||
"yootheme/aboutImage/young-tourist-sitting-tent.jpg",
|
||||
"yootheme/aboutImage/portrait-young-male-tourist-standing-forest-with-tent.jpg"
|
||||
],
|
||||
"images": {
|
||||
"mainImage1": "yootheme/img/a1.jpg",
|
||||
"mainImage2": "yootheme/img/a2.jpg"
|
||||
},
|
||||
"content": {
|
||||
"sectionTitle": "About Us",
|
||||
"mainTitle": "Creating Amazing Camps",
|
||||
"description": "Learning is closely tied to practical experience—summer is the perfect time for hands-on opportunities. While knowledge must still be nurtured, it can take on new and more engaging forms.",
|
||||
"quote": "Your Journey, Your Comfort,\nYour Adventure.",
|
||||
"authorText": "Adventurer with\nhappy customer",
|
||||
"targetCount": 50
|
||||
},
|
||||
"features": [
|
||||
"Fun-Filled Experiences for Every Camper",
|
||||
"Adventures That Inspire Confidence and Growth",
|
||||
"Memories and Friendships That Last a Lifetime"
|
||||
],
|
||||
"button": {
|
||||
"label": "Learn More About",
|
||||
"href": "/info/about"
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
{
|
||||
"hero": {
|
||||
"title": "About Us",
|
||||
"backgroundImage": "/uploads/about/banner.jpg"
|
||||
},
|
||||
|
||||
"introduction": {
|
||||
"subtitle": "Go & Grow Camp",
|
||||
"title": "Go & Grow Camp A Place to Learn, Connect, and Grow",
|
||||
"description": "Go & Grow Camp brings together young people from different countries and cultures to enjoy fun activities, meaningful projects, and positive community experiences. Every camper—new or returning—quickly feels included thanks to our welcoming environment and supportive team.",
|
||||
"mainImage": "/uploads/about/section2.jpg",
|
||||
"services": [
|
||||
{
|
||||
"title": "Always Here",
|
||||
"description": "Camp leaders are ready to guide and support you whenever needed."
|
||||
},
|
||||
{
|
||||
"title": "Fun & Learning",
|
||||
"description": "Engage in exciting activities that help you grow new skills."
|
||||
},
|
||||
{
|
||||
"title": "Team Spirit",
|
||||
"description": "Work together, take responsibility, and support each other at camp."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"statistics": {
|
||||
"items": [
|
||||
{
|
||||
"number": "2K+",
|
||||
"description": "Smiles and Friendships Made"
|
||||
},
|
||||
{
|
||||
"number": "25+",
|
||||
"description": "Countries Connected"
|
||||
},
|
||||
{
|
||||
"number": "50+",
|
||||
"description": "Adventure & Skill-Building Activities"
|
||||
},
|
||||
{
|
||||
"number": "20+",
|
||||
"description": "Exciting Challenges Every Camp"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"accommodation": {
|
||||
"subtitle": "Accommodation",
|
||||
"title": "Blending Comfort With Responsible Living",
|
||||
"description": "Enjoy a tranquil atmosphere with beautiful views, modern facilities, and personal touches that make you feel at home.",
|
||||
"features": [
|
||||
{
|
||||
"title": "Safe Environment",
|
||||
"description": "Safety is our top priority, with secure facilities and connecting with nature.",
|
||||
"icon": "/uploads/about/act2.jpg"
|
||||
},
|
||||
{
|
||||
"title": "Family Atmosphere",
|
||||
"description": "Every camper is part of our big camp family, where friendships grow and everyone feels included.",
|
||||
"icon": "/uploads/about/act2.jpg"
|
||||
},
|
||||
{
|
||||
"title": "Cultural Exchange",
|
||||
"description": "Experience diversity and learn about different cultures from campers around the world.",
|
||||
"icon": "/uploads/about/act2.jpg"
|
||||
},
|
||||
{
|
||||
"title": "Personal Growth",
|
||||
"description": "Activities encourage confidence, independence, and learning through fun challenges.",
|
||||
"icon": "/uploads/about/act2.jpg"
|
||||
},
|
||||
{
|
||||
"title": "Creativity & Fun",
|
||||
"description": "Express yourself through games, arts, and exciting hands-on experiences.",
|
||||
"icon": "/uploads/about/act2.jpg"
|
||||
},
|
||||
{
|
||||
"title": "Creativity & Fun",
|
||||
"description": "Express yourself through games, arts, and exciting hands-on experiences.",
|
||||
"icon": "/uploads/about/act2.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"activities": {
|
||||
"subtitle": "Activities",
|
||||
"title": "Enjoy unforgettable experiences at Go and Grow Camp",
|
||||
"description": "Discover a world of adventure, creativity, and friendship. From exciting outdoor activities to hands-on workshops, every day is full of new experiences that help campers grow, have fun, and make memories that last a lifetime.",
|
||||
"gallery": [
|
||||
{
|
||||
"image": "/uploads/about/act1.jpg",
|
||||
"title": "Outdoor Adventures",
|
||||
"description": "Climb, paddle and explore with our experienced team."
|
||||
},
|
||||
{
|
||||
"image": "/uploads/about/act2.jpg",
|
||||
"title": "Creative Workshops",
|
||||
"description": "Arts & crafts sessions to spark imagination."
|
||||
},
|
||||
{
|
||||
"image": "/uploads/about/act3.jpg",
|
||||
"title": "Water Sports",
|
||||
"description": "Safe swimming and supervised water activities."
|
||||
},
|
||||
{
|
||||
"image": "/uploads/about/act4.jpg",
|
||||
"title": "Campfire Nights",
|
||||
"description": "Evening stories, music, and marshmallow roasting."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"newsletter": {
|
||||
"imagePath": "/uploads/about/newsletter.jpg",
|
||||
"title": "Stay Updated with Our Monthly",
|
||||
"description": "Sign up to receive the latest news about new camps, activities, and exciting opportunities. Don't miss out on anything fun!",
|
||||
"buttonText": "Subscribe"
|
||||
},
|
||||
|
||||
"events": {
|
||||
"title": "Tour Events for you",
|
||||
"items": [
|
||||
|
||||
{
|
||||
"imageUrl": "/uploads/about/act1.jpg",
|
||||
"date": "September 19, 2022",
|
||||
"title": "The Bottom Line on Dietary Supplements",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris sit amet tristique neque, in suscipit sem. Fusce ut tellus neque. Suspendisse ...",
|
||||
"age": "Age Group: 10–14"
|
||||
},
|
||||
{
|
||||
"imageUrl": "/uploads/about/act2.jpg",
|
||||
"date": "September 19, 2022",
|
||||
"title": "The Bottom Line on Dietary Supplements",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris sit amet tristique neque, in suscipit sem. Fusce ut tellus neque. Suspendisse ...",
|
||||
"age": "Age Group: 10–14"
|
||||
},
|
||||
{
|
||||
"imageUrl": "/uploads/about/act3.jpg",
|
||||
"date": "September 19, 2022",
|
||||
"title": "The Bottom Line on Dietary Supplements",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris sit amet tristique neque, in suscipit sem. Fusce ut tellus neque. Suspendisse ...",
|
||||
"age": "Age Group: 10–14"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
|
||||
"hero": {
|
||||
"title": "From Application to Visa – We've Got You Covered",
|
||||
"title": "From Application to Visa – We’ve Got You Covered",
|
||||
"subtitle": "Global Education Simplified",
|
||||
"description": "We guide you through every step of the education visa process, from initial application to final approval, ensuring a smooth, hassle-free journey.",
|
||||
"primaryButton": {
|
||||
@@ -48,25 +47,25 @@
|
||||
"number": "01",
|
||||
"title": "Student Visa Guidance",
|
||||
"description": "Assistance with admission, documentation, and visa application.Assistance",
|
||||
"link": "/service-details"
|
||||
"link": "/services/student-visa"
|
||||
},
|
||||
{
|
||||
"number": "02",
|
||||
"title": "PTE Exam Preparation",
|
||||
"description": "We provide expert guidance and personalized support throughout the education visa process,",
|
||||
"link": "/service-details"
|
||||
"link": "/services/pte-exam"
|
||||
},
|
||||
{
|
||||
"number": "03",
|
||||
"title": "University Selection Assistance",
|
||||
"description": "We provide expert guidance and personalized support throughout the education visa process,",
|
||||
"link": "/service-details"
|
||||
"link": "/services/university-selection"
|
||||
},
|
||||
{
|
||||
"number": "04",
|
||||
"title": "IELTS Exam Preparation",
|
||||
"description": "We provide expert guidance and personalized support throughout the education visa process,",
|
||||
"link": "/service-details"
|
||||
"link": "/services/ielts-exam"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app_cms_ggcamp:
|
||||
build: .
|
||||
container_name: my_node_app_cms_ggcamp
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
volumes:
|
||||
- .:/home/node/app
|
||||
- /home/node/app/node_modules
|
||||
network_mode: host
|
||||
@@ -1,64 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const aboutSchema = new mongoose.Schema({
|
||||
banner: {
|
||||
image: String,
|
||||
title: String,
|
||||
text: String
|
||||
},
|
||||
about: {
|
||||
title: String,
|
||||
paragraphs: [String],
|
||||
list_items: [String],
|
||||
button: {
|
||||
text: String,
|
||||
url: String
|
||||
},
|
||||
image: String,
|
||||
quote: {
|
||||
mark_image: String,
|
||||
title: String,
|
||||
text: String,
|
||||
author: String
|
||||
}
|
||||
},
|
||||
values: {
|
||||
background_image: String,
|
||||
items: [{
|
||||
icon: String,
|
||||
title: String,
|
||||
text: String
|
||||
}]
|
||||
},
|
||||
education: {
|
||||
images: {
|
||||
student1: String,
|
||||
student2: String
|
||||
},
|
||||
subtitle: String,
|
||||
title: String,
|
||||
text: String
|
||||
},
|
||||
advantages: {
|
||||
title: String,
|
||||
items: [{
|
||||
number: String,
|
||||
title: String,
|
||||
text: String
|
||||
}]
|
||||
},
|
||||
academic_board: {
|
||||
title: String,
|
||||
members: [{
|
||||
image: String,
|
||||
title: String,
|
||||
name: String,
|
||||
color: String
|
||||
}]
|
||||
},
|
||||
updatedAt: Date
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('About', aboutSchema);
|
||||
@@ -2,87 +2,105 @@ const mongoose = require("mongoose");
|
||||
|
||||
const aboutUsSchema = new mongoose.Schema(
|
||||
{
|
||||
// Hero section
|
||||
hero: {
|
||||
title: String,
|
||||
breadcrumb: [String],
|
||||
backgroundImage: String,
|
||||
},
|
||||
|
||||
// Introduction section with nested services
|
||||
introduction: {
|
||||
subtitle: String,
|
||||
title: String,
|
||||
intro: {
|
||||
subheading: String,
|
||||
heading: String,
|
||||
description: String,
|
||||
mainImage: String,
|
||||
services: [
|
||||
{
|
||||
title: String,
|
||||
description: String,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Statistics with nested items
|
||||
statistics: {
|
||||
items: [
|
||||
{
|
||||
number: String,
|
||||
description: String,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Accommodation section with nested features
|
||||
accommodation: {
|
||||
subtitle: String,
|
||||
title: String,
|
||||
description: String,
|
||||
features: [
|
||||
{
|
||||
title: String,
|
||||
description: String,
|
||||
icon: String,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Activities section with nested gallery
|
||||
activities: {
|
||||
subtitle: String,
|
||||
title: String,
|
||||
description: String,
|
||||
gallery: [
|
||||
{
|
||||
image: String,
|
||||
title: String,
|
||||
},
|
||||
mission: {
|
||||
subheading: String,
|
||||
heading: String,
|
||||
description: String,
|
||||
images: {
|
||||
main: String,
|
||||
secondary: String,
|
||||
bgShape: String,
|
||||
planeShape: String,
|
||||
topShape: String,
|
||||
globeShape: String,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Newsletter
|
||||
newsletter: {
|
||||
imagePath: String,
|
||||
title: String,
|
||||
description: String,
|
||||
buttonText: String,
|
||||
},
|
||||
|
||||
// Events with nested items
|
||||
events: {
|
||||
title: String,
|
||||
items: [
|
||||
new mongoose.Schema(
|
||||
{
|
||||
imageUrl: String,
|
||||
date: String,
|
||||
icon: String,
|
||||
label: String,
|
||||
description: String,
|
||||
},
|
||||
{ _id: false }
|
||||
),
|
||||
],
|
||||
features: [String],
|
||||
ctaButton: {
|
||||
label: String,
|
||||
href: String,
|
||||
},
|
||||
},
|
||||
features: {
|
||||
backgroundImage: String,
|
||||
subheading: String,
|
||||
heading: String,
|
||||
description: String,
|
||||
image: String,
|
||||
items: [
|
||||
new mongoose.Schema(
|
||||
{
|
||||
icon: String,
|
||||
title: String,
|
||||
description: String,
|
||||
age: String,
|
||||
},
|
||||
{ _id: false }
|
||||
),
|
||||
],
|
||||
ctaButton: {
|
||||
label: String,
|
||||
href: String,
|
||||
},
|
||||
},
|
||||
news: {
|
||||
subheading: String,
|
||||
heading: String,
|
||||
ctaButton: {
|
||||
label: String,
|
||||
href: String,
|
||||
},
|
||||
items: [
|
||||
new mongoose.Schema(
|
||||
{
|
||||
title: String,
|
||||
category: String,
|
||||
date: String,
|
||||
comments: Number,
|
||||
author: {
|
||||
name: String,
|
||||
avatar: String,
|
||||
},
|
||||
link: String,
|
||||
thumbnail: String,
|
||||
},
|
||||
{ _id: false }
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
{timestamps: true}
|
||||
{
|
||||
timestamps: true,
|
||||
collection: "aboutus",
|
||||
}
|
||||
);
|
||||
|
||||
// Static method để đảm bảo luôn chỉ có 1 bản ghi duy nhất (Singleton)
|
||||
aboutUsSchema.statics.getSingle = async function () {
|
||||
let doc = await this.findOne();
|
||||
if (!doc) {
|
||||
doc = await this.create({});
|
||||
}
|
||||
return doc;
|
||||
};
|
||||
|
||||
module.exports = mongoose.model("AboutUs", aboutUsSchema);
|
||||
|
||||
@@ -11,14 +11,35 @@ const LinkSchema = new Schema(
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const HeroSchema = new Schema(
|
||||
// Hero slide (for multiple hero items in slider)
|
||||
const HeroSlideSchema = new Schema(
|
||||
{
|
||||
title: { type: String, default: "" },
|
||||
subtitle: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
primaryButton: { type: LinkSchema, default: () => ({}) },
|
||||
secondaryButton: { type: LinkSchema, default: () => ({}) },
|
||||
heroImage: { type: String, default: "" },
|
||||
videoUrl: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const HeroSchema = new Schema(
|
||||
{
|
||||
// Background for whole hero section
|
||||
backgroundImage: { type: String, default: "" },
|
||||
|
||||
// Multiple slides
|
||||
slides: { type: [HeroSlideSchema], default: [] },
|
||||
|
||||
// Legacy single-slide fields (backward compatible)
|
||||
title: { type: String, default: "" },
|
||||
subtitle: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
primaryButton: { type: LinkSchema, default: () => ({}) },
|
||||
secondaryButton: { type: LinkSchema, default: () => ({}) },
|
||||
heroImage: { type: String, default: "" },
|
||||
videoUrl: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
@@ -38,6 +59,9 @@ const WhyChooseUsSchema = new Schema(
|
||||
heading: { type: String, default: "" },
|
||||
subheading: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
highlightWord: { type: String, default: "" },
|
||||
mainImage: { type: String, default: "" },
|
||||
secondaryImage: { type: String, default: "" },
|
||||
items: { type: [WhyChooseUsItemSchema], default: [] },
|
||||
features: { type: [String], default: [] },
|
||||
ctaButton: { type: LinkSchema, default: () => ({}) },
|
||||
|
||||
2891
package-lock.json
generated
2891
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@ const { ensureAuthenticated } = require("../middleware/auth");
|
||||
const dashboardController = require("../controllers/dashboardController");
|
||||
const uploadController = require("../controllers/uploadController");
|
||||
const homeController = require("../controllers/homeController");
|
||||
const aboutController = require("../controllers/aboutController");
|
||||
const headerController = require("../controllers/headerController");
|
||||
const footerController = require("../controllers/footerController");
|
||||
const aboutUsController = require("../controllers/aboutUsController");
|
||||
@@ -48,18 +47,9 @@ router.param("code", (req, res, next, code) => {
|
||||
next();
|
||||
});
|
||||
|
||||
// About
|
||||
router.get("/about", ensureAuthenticated, aboutController.index);
|
||||
router.post("/about/update", ensureAuthenticated, aboutController.update);
|
||||
|
||||
// AboutUs admin CRUD
|
||||
// About Us
|
||||
router.get("/about-us", ensureAuthenticated, aboutUsController.index);
|
||||
router.get("/about-us/create", ensureAuthenticated, aboutUsController.createForm);
|
||||
router.post("/about-us/create", ensureAuthenticated, aboutUsController.create);
|
||||
router.get("/about-us/:id/edit", ensureAuthenticated, aboutUsController.editForm);
|
||||
router.post("/about-us/:id/update", ensureAuthenticated, aboutUsController.update);
|
||||
router.post("/about-us/:id/delete", ensureAuthenticated, aboutUsController.delete);
|
||||
router.get("/about-us/:id/preview", ensureAuthenticated, aboutUsController.preview);
|
||||
router.post("/about-us/update", ensureAuthenticated, aboutUsController.update);
|
||||
|
||||
// Booking admin CRUD removed
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ const express = require("express");
|
||||
const path = require("path");
|
||||
const router = express.Router();
|
||||
const homeController = require("../controllers/homeController");
|
||||
const aboutController = require("../controllers/aboutController");
|
||||
const aboutUsController = require("../controllers/aboutUsController");
|
||||
const headerController = require("../controllers/headerController");
|
||||
const socialLinkController = require("../controllers/socialLinkController");
|
||||
@@ -38,12 +37,12 @@ router.get("/", (req, res) => {
|
||||
router.get("/api/home", homeController.api);
|
||||
|
||||
// API để lấy dữ liệu about
|
||||
router.get("/api/about", aboutController.api);
|
||||
router.get("/api/about", aboutUsController.getAbout);
|
||||
router.put("/api/about", aboutUsController.updateAbout);
|
||||
|
||||
// Public about-us page and API (aboutUs.json flow)
|
||||
router.get("/about-us", aboutUsController.page);
|
||||
// Return a list/array of AboutUs records for frontend consumption
|
||||
router.get("/api/about-us", aboutUsController.apiList);
|
||||
// Public about-us page and API (legacy support)
|
||||
router.get("/about-us", aboutUsController.getAbout);
|
||||
router.get("/api/about-us", aboutUsController.getAbout);
|
||||
|
||||
// Header API route
|
||||
router.get("/api/header", headerController.api);
|
||||
|
||||
48
scripts/migrateAboutUs.js
Normal file
48
scripts/migrateAboutUs.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const mongoose = require("mongoose");
|
||||
const dotenv = require("dotenv");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
const AboutUs = require("../models/aboutUs");
|
||||
|
||||
const migrate = async () => {
|
||||
try {
|
||||
console.log("🚀 Starting About Us migration...");
|
||||
|
||||
// 1. Connect to MongoDB
|
||||
await mongoose.connect(process.env.MONGODB_URI);
|
||||
console.log("✅ MongoDB Connected");
|
||||
|
||||
// 2. Read about.json from Backend (Source of Truth)
|
||||
const jsonPath = path.join(__dirname, "../data/about.json");
|
||||
if (!fs.existsSync(jsonPath)) {
|
||||
throw new Error(`Source about.json not found at: ${jsonPath}`);
|
||||
}
|
||||
|
||||
const rawData = fs.readFileSync(jsonPath, "utf8");
|
||||
const jsonData = JSON.parse(rawData);
|
||||
console.log("✅ Read about.json successfully");
|
||||
|
||||
// 3. Delete existing AboutUs documents (Singleton pattern)
|
||||
await AboutUs.deleteMany({});
|
||||
console.log("✅ Cleared existing AboutUs collection");
|
||||
|
||||
// 4. Create new AboutUs document with JSON data
|
||||
const newAboutUs = new AboutUs(jsonData);
|
||||
await newAboutUs.save();
|
||||
console.log("✅ Successfully migrated about.json data to MongoDB");
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ Migration failed:", error.message);
|
||||
} finally {
|
||||
// 5. Close connection
|
||||
await mongoose.connection.close();
|
||||
console.log("👋 Database connection closed");
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
migrate();
|
||||
52
scripts/seedAbout.js
Normal file
52
scripts/seedAbout.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const mongoose = require("mongoose");
|
||||
const dotenv = require("dotenv");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
const AboutUs = require("../models/aboutUs");
|
||||
|
||||
const seedAbout = async () => {
|
||||
try {
|
||||
console.log("🚀 Starting About section seeding...");
|
||||
|
||||
// 1. Connect to MongoDB
|
||||
if (!process.env.MONGODB_URI) {
|
||||
throw new Error("MONGODB_URI is not defined in environment variables");
|
||||
}
|
||||
await mongoose.connect(process.env.MONGODB_URI);
|
||||
console.log("✅ MongoDB Connected");
|
||||
|
||||
// 2. Read about.json (Single Source of Truth)
|
||||
const jsonPath = path.join(__dirname, "../data/about.json");
|
||||
if (!fs.existsSync(jsonPath)) {
|
||||
throw new Error(`Source about.json not found at: ${jsonPath}`);
|
||||
}
|
||||
|
||||
const rawData = fs.readFileSync(jsonPath, "utf8");
|
||||
const jsonData = JSON.parse(rawData);
|
||||
console.log("✅ Read data/about.json successfully");
|
||||
|
||||
// 3. Upsert logic (Singleton pattern)
|
||||
// We look for any existing document and update it, or create a new one if none exists.
|
||||
await AboutUs.findOneAndUpdate(
|
||||
{},
|
||||
jsonData,
|
||||
{ upsert: true, new: true, setDefaultsOnInsert: true }
|
||||
);
|
||||
|
||||
console.log("✅ Successfully seeded about.json data to MongoDB (Upserted)");
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ Seeding failed:", error.message);
|
||||
} finally {
|
||||
// 4. Close connection
|
||||
await mongoose.connection.close();
|
||||
console.log("👋 Database connection closed");
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
seedAbout();
|
||||
@@ -44,6 +44,9 @@ app.use(
|
||||
express.static(path.join(__dirname, "assets")),
|
||||
);
|
||||
|
||||
// Map /assets/img to public/img to support frontend paths
|
||||
app.use("/assets/img", express.static(path.join(__dirname, "public", "img")));
|
||||
|
||||
// Serve public folder at root (for /js, /img, etc.)
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* @returns {Object} - Dữ liệu đã được xử lý với đường dẫn hình ảnh đầy đủ
|
||||
*/
|
||||
function addBaseUrlToImages(data, baseUrl) {
|
||||
// baseUrl can be passed explicitly (e.g., from req), otherwise fall back to env
|
||||
const BACKEND_URL = baseUrl || process.env.BACKEND_URL || "";
|
||||
// Use passed baseUrl, then env var, then default to localhost:3001
|
||||
const BACKEND_URL = (baseUrl || process.env.BACKEND_URL || "http://localhost:3001").replace(/\/$/, "");
|
||||
|
||||
// Tạo bản sao sâu để tránh thay đổi dữ liệu gốc
|
||||
const processedData = JSON.parse(JSON.stringify(data));
|
||||
@@ -14,16 +14,27 @@ function addBaseUrlToImages(data, baseUrl) {
|
||||
const processObject = (obj) => {
|
||||
if (!obj || typeof obj !== "object") return;
|
||||
|
||||
Object.keys(obj).forEach((key) => {
|
||||
// Kiểm tra nếu thuộc tính chứa đường dẫn hình ảnh bắt đầu bằng /uploads/
|
||||
if (typeof obj[key] === "string" && obj[key].startsWith("/uploads/")) {
|
||||
// Thêm BACKEND_URL nếu đường dẫn chưa có http
|
||||
if (!obj[key].startsWith("http")) {
|
||||
obj[key] = `${BACKEND_URL}${obj[key]}`;
|
||||
if (Array.isArray(obj)) {
|
||||
obj.forEach((item, index) => {
|
||||
if (typeof item === "string" && (item.startsWith("/uploads/") || item.startsWith("/assets/img/"))) {
|
||||
if (!item.startsWith("http")) {
|
||||
obj[index] = `${BACKEND_URL}${item}`;
|
||||
}
|
||||
} else if (typeof obj[key] === "object") {
|
||||
// Đệ quy xử lý các đối tượng và mảng lồng nhau
|
||||
processObject(obj[key]);
|
||||
} else if (typeof item === "object") {
|
||||
processObject(item);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(obj).forEach((key) => {
|
||||
const value = obj[key];
|
||||
if (typeof value === "string" && (value.startsWith("/uploads/") || value.startsWith("/assets/img/"))) {
|
||||
if (!value.startsWith("http")) {
|
||||
obj[key] = `${BACKEND_URL}${value}`;
|
||||
}
|
||||
} else if (value && typeof value === "object") {
|
||||
processObject(value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -269,6 +269,11 @@
|
||||
const targetId = event.target.getAttribute('href').substring(1);
|
||||
document.getElementById('activeTabInput').value = targetId;
|
||||
|
||||
// Update URL without reload to preserve tab state
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('tab', targetId);
|
||||
window.history.replaceState({}, '', url);
|
||||
|
||||
// Only load Menu Tree if clicking on the menu tab
|
||||
if (targetId === 'menu') {
|
||||
loadMenuTree();
|
||||
@@ -365,6 +370,12 @@
|
||||
showNotification('All changes saved successfully', 'success');
|
||||
submitBtn.classList.remove('btn-primary');
|
||||
submitBtn.classList.add('btn-outline-primary');
|
||||
|
||||
// Reload to refresh data, preserve current tab
|
||||
const currentTab = document.getElementById('activeTabInput').value;
|
||||
setTimeout(() => {
|
||||
window.location.href = window.location.pathname + '?tab=' + currentTab;
|
||||
}, 1000);
|
||||
} else {
|
||||
const errorMsg = (!headerResult.success ? headerResult.message : '') || (!menuResult.success ? menuResult.message : '') || 'Unable to save some changes';
|
||||
showNotification('Error: ' + errorMsg, 'error');
|
||||
@@ -1113,19 +1124,29 @@
|
||||
console.log('Response:', response.data);
|
||||
|
||||
if (response.data.success || response.status === 200) {
|
||||
showToast('Success', 'Menu information has been updated', 'success');
|
||||
showNotification('Menu item saved successfully', 'success');
|
||||
|
||||
// Hide modal
|
||||
const modalElement = document.getElementById('modalAddMenu');
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(modalElement);
|
||||
modal.hide();
|
||||
// Refresh data or reload tab
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
|
||||
// Mark as changed so user needs to click Save Changes
|
||||
if (typeof window.markHeaderChanged === 'function') {
|
||||
window.markHeaderChanged();
|
||||
}
|
||||
|
||||
// Reload page to show updated menu structure, preserve current tab
|
||||
const currentTab = document.getElementById('activeTabInput').value;
|
||||
setTimeout(() => {
|
||||
window.location.href = window.location.pathname + '?tab=' + currentTab;
|
||||
}, 1000);
|
||||
} else {
|
||||
showToast('Error', response.data.message || 'Unable to save menu', 'error');
|
||||
showNotification(response.data.message || 'Unable to save menu', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('AJAX Error:', error);
|
||||
showToast('Error', 'Server connection error: ' + (error.response?.data?.message || error.message), 'error');
|
||||
showNotification('Server connection error: ' + (error.response?.data?.message || error.message), 'error');
|
||||
} finally {
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<span class="badge bg-light text-dark border ms-2" style="font-size: 0.7rem;">External</span>
|
||||
<% } %>
|
||||
<% if (item.status === 'inactive') { %>
|
||||
<span class="badge bg-soft-secondary ms-2">Inactive</span>
|
||||
<span class="badge ms-2 bg-soft-danger text-danger">Inactive</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-soft-success ms-2">Active</span>
|
||||
<% } %>
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const form = document.querySelector("form");
|
||||
if (form) {
|
||||
form.addEventListener("submit", function(e) {
|
||||
form.addEventListener("submit", function (e) {
|
||||
console.log("Form submitting, collecting data from scrapers...");
|
||||
|
||||
// Tự động thu gom dữ liệu từ các section đã đăng ký
|
||||
@@ -164,7 +164,7 @@
|
||||
// --- UTILITIES (Dùng chung) ---
|
||||
|
||||
function initImageUploads() {
|
||||
document.addEventListener("click", function(e) {
|
||||
document.addEventListener("click", function (e) {
|
||||
const btn = e.target.closest(".btn-upload-image");
|
||||
if (btn) {
|
||||
document.getElementById("currentImageType").value = btn.dataset.imageType;
|
||||
|
||||
@@ -13,32 +13,18 @@
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="faqHeading"
|
||||
value="<%= data.faq?.heading || '' %>"
|
||||
placeholder="e.g., Got Questions? We've Got Answers"
|
||||
/>
|
||||
<input type="text" class="form-control" id="faqHeading" value="<%= data.faq?.heading || '' %>"
|
||||
placeholder="e.g., Got Questions? We've Got Answers" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subheading</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="faqSubheading"
|
||||
value="<%= data.faq?.subheading || '' %>"
|
||||
placeholder="e.g., Visa FAQs"
|
||||
/>
|
||||
<input type="text" class="form-control" id="faqSubheading" value="<%= data.faq?.subheading || '' %>"
|
||||
placeholder="e.g., Visa FAQs" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="faqDescription"
|
||||
rows="3"
|
||||
placeholder="Enter description"
|
||||
><%= data.faq?.description || '' %></textarea>
|
||||
<textarea class="form-control" id="faqDescription" rows="3"
|
||||
placeholder="Enter description"><%= data.faq?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,16 +34,33 @@
|
||||
<!-- FAQ Items -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-question-circle me-2"></i>FAQ Items
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body" id="faqItemsContainer">
|
||||
<%
|
||||
const faqItems = (data.faq && Array.isArray(data.faq.items) && data.faq.items.length === 5)
|
||||
? data.faq.items
|
||||
: (data.faq && Array.isArray(data.faq.items) && data.faq.items.length > 0)
|
||||
? (function () {
|
||||
const clone = data.faq.items.slice(0, 5);
|
||||
while (clone.length < 5) clone.push({});
|
||||
return clone;
|
||||
})()
|
||||
: [{}, {}, {}, {}, {}];
|
||||
%>
|
||||
<% faqItems.forEach(function(item, index) { %>
|
||||
<div class="card mb-3 bg-light border faq-item" data-index="<%= index %>">
|
||||
<div class="card-header d-flex justify-content-between align-items-center bg-white">
|
||||
<h6 class="card-title fw-bold mb-0">FAQ
|
||||
<span class="faq-item-label">
|
||||
<%= index + 1 %>
|
||||
</span>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% (data.faq?.items || []).forEach(function(item, index) { %>
|
||||
<div class="card mb-3 bg-light border">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title fw-bold mb-3">FAQ <%= index + 1 %></h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Question</label>
|
||||
@@ -98,23 +101,13 @@
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Label</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="faqCtaLabel"
|
||||
value="<%= data.faq?.ctaButton?.label || '' %>"
|
||||
placeholder="e.g., contact us"
|
||||
/>
|
||||
<input type="text" class="form-control" id="faqCtaLabel" value="<%= data.faq?.ctaButton?.label || '' %>"
|
||||
placeholder="e.g., contact us" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="faqCtaHref"
|
||||
value="<%= data.faq?.ctaButton?.href || '' %>"
|
||||
placeholder="/contact"
|
||||
/>
|
||||
<input type="text" class="form-control" id="faqCtaHref" value="<%= data.faq?.ctaButton?.href || '' %>"
|
||||
placeholder="/contact" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,3 +115,34 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Register scraper to collect FAQ data before form submit
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
window.homeScrapers.faq = () => {
|
||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||
|
||||
const items = [];
|
||||
document.querySelectorAll(".faq-item").forEach((el, idx) => {
|
||||
const index = el.getAttribute("data-index") || idx;
|
||||
|
||||
const question = getVal(`faqQuestion_${index}`);
|
||||
const answer = getVal(`faqAnswer_${index}`);
|
||||
|
||||
if (question || answer) {
|
||||
items.push({ question, answer });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
heading: getVal("faqHeading"),
|
||||
subheading: getVal("faqSubheading"),
|
||||
description: getVal("faqDescription"),
|
||||
ctaButton: {
|
||||
label: getVal("faqCtaLabel"),
|
||||
href: getVal("faqCtaHref")
|
||||
},
|
||||
items
|
||||
};
|
||||
};
|
||||
</script>
|
||||
@@ -1,119 +1,132 @@
|
||||
<!-- Hero Tab -->
|
||||
<div class="tab-pane fade show active" id="hero" role="tabpanel">
|
||||
<div class="row g-4">
|
||||
<!-- Basic Info -->
|
||||
<!-- Background Image (section-level) -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<div class="card border shadow-sm mb-3">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||
<i class="fas fa-image me-2"></i>Hero Background
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="heroTitle"
|
||||
value="<%= data.hero?.title || '' %>"
|
||||
placeholder="e.g., From Application to Visa"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subtitle</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="heroSubtitle"
|
||||
value="<%= data.hero?.subtitle || '' %>"
|
||||
placeholder="e.g., Global Education Simplified"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="heroDescription"
|
||||
rows="3"
|
||||
placeholder="Enter hero description"
|
||||
><%= data.hero?.description || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Background Image</label>
|
||||
<div class="input-group mb-2">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="heroBackgroundImage"
|
||||
value="<%= data.hero?.backgroundImage || '' %>"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="heroBackgroundImage"
|
||||
data-image-type="home"
|
||||
>
|
||||
<input type="text" class="form-control" id="heroBackgroundImage"
|
||||
value="<%= data.hero?.backgroundImage || '' %>" placeholder="/assets/img/home-1/hero/bg.jpg" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="heroBackgroundImage" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<% if (data.hero?.backgroundImage) { %>
|
||||
<div class="mt-2">
|
||||
<img
|
||||
src="<%= data.hero.backgroundImage %>"
|
||||
class="img-thumbnail"
|
||||
style="height: 200px; width: 100%; object-fit: cover;"
|
||||
alt="Background preview"
|
||||
/>
|
||||
<img src="<%= data.hero.backgroundImage %>" class="img-thumbnail"
|
||||
style="height: 200px; width: 100%; object-fit: cover;" alt="Background preview" />
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Video URL</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="heroVideoUrl"
|
||||
value="<%= data.hero?.videoUrl || '' %>"
|
||||
placeholder="https://www.youtube.com/watch?v=..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Primary Button -->
|
||||
<div class="col-md-6">
|
||||
<!-- Slides -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-mouse-pointer me-2"></i>Primary Button
|
||||
<i class="fas fa-sliders-h me-2"></i>Hero Slides
|
||||
</h6>
|
||||
<button type="button" class="btn btn-sm btn-primary" id="addHeroSlideBtn">
|
||||
<i class="fas fa-plus me-1"></i>Add Slide
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body" id="heroSlidesContainer">
|
||||
<% const existingSlides=(data.hero && Array.isArray(data.hero.slides) && data.hero.slides.length> 0)
|
||||
? data.hero.slides
|
||||
: [{
|
||||
title: data.hero?.title || '',
|
||||
subtitle: data.hero?.subtitle || '',
|
||||
description: data.hero?.description || '',
|
||||
heroImage: data.hero?.heroImage || '',
|
||||
videoUrl: data.hero?.videoUrl || '',
|
||||
primaryButton: data.hero?.primaryButton || {},
|
||||
secondaryButton: data.hero?.secondaryButton || {},
|
||||
}];
|
||||
%>
|
||||
|
||||
<% existingSlides.forEach(function(slide, index) { %>
|
||||
<div class="card mb-3 border hero-slide-item" data-index="<%= index %>">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Slide
|
||||
<span class="hero-slide-label">
|
||||
<%= index + 1 %>
|
||||
</span>
|
||||
</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger btn-remove-hero-slide">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_title"
|
||||
value="<%= slide.title || '' %>" placeholder="e.g., From Application to Visa" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subtitle</label>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_subtitle"
|
||||
value="<%= slide.subtitle || '' %>" placeholder="e.g., Global Education Simplified" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" id="heroSlide_<%= index %>_description" rows="3"
|
||||
placeholder="Enter hero description"><%= slide.description || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Hero Image</label>
|
||||
<small class="text-muted d-block mb-1">Recommended size: 893x848px</small>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_heroImage"
|
||||
value="<%= slide.heroImage || '' %>" placeholder="/assets/img/home-1/hero/man.png" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="heroSlide_<%= index %>_heroImage" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<% if (slide.heroImage) { %>
|
||||
<div class="mt-2">
|
||||
<img src="<%= slide.heroImage %>" class="img-thumbnail"
|
||||
style="height: 200px; width: 100%; object-fit: cover;" alt="Hero image preview" />
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Video URL</label>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_videoUrl"
|
||||
value="<%= slide.videoUrl || '' %>" placeholder="https://www.youtube.com/watch?v=..." />
|
||||
</div>
|
||||
|
||||
<!-- Primary Button -->
|
||||
<div class="col-md-6">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<h6 class="fw-semibold mb-3">
|
||||
<i class="fas fa-mouse-pointer me-2"></i>Primary Button
|
||||
</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Label</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="heroPrimaryButtonLabel"
|
||||
value="<%= data.hero?.primaryButton?.label || '' %>"
|
||||
placeholder="e.g., Apply now"
|
||||
/>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_primaryLabel"
|
||||
value="<%= slide.primaryButton?.label || '' %>" placeholder="e.g., Apply now" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="heroPrimaryButtonHref"
|
||||
value="<%= data.hero?.primaryButton?.href || '' %>"
|
||||
placeholder="/contact"
|
||||
/>
|
||||
</div>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_primaryHref"
|
||||
value="<%= slide.primaryButton?.href || '' %>" placeholder="/contact" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,37 +134,179 @@
|
||||
|
||||
<!-- Secondary Button -->
|
||||
<div class="col-md-6">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<h6 class="fw-semibold mb-3">
|
||||
<i class="fas fa-mouse-pointer me-2"></i>Secondary Button
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Label</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="heroSecondaryButtonLabel"
|
||||
value="<%= data.hero?.secondaryButton?.label || '' %>"
|
||||
placeholder="e.g., Book Free Consultation"
|
||||
/>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_secondaryLabel"
|
||||
value="<%= slide.secondaryButton?.label || '' %>"
|
||||
placeholder="e.g., Book Free Consultation" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="heroSecondaryButtonHref"
|
||||
value="<%= data.hero?.secondaryButton?.href || '' %>"
|
||||
placeholder="/contact"
|
||||
/>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_secondaryHref"
|
||||
value="<%= slide.secondaryButton?.href || '' %>" placeholder="/contact" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Register scraper to collect Hero data before form submit
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
window.homeScrapers.hero = () => {
|
||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||
|
||||
const backgroundImage = getVal("heroBackgroundImage");
|
||||
|
||||
const slides = [];
|
||||
const slideEls = document.querySelectorAll(".hero-slide-item");
|
||||
slideEls.forEach((el, idx) => {
|
||||
const index = el.getAttribute("data-index") || idx;
|
||||
const prefix = `heroSlide_${index}_`;
|
||||
|
||||
const slide = {
|
||||
title: getVal(prefix + "title"),
|
||||
subtitle: getVal(prefix + "subtitle"),
|
||||
description: getVal(prefix + "description"),
|
||||
heroImage: getVal(prefix + "heroImage"),
|
||||
videoUrl: getVal(prefix + "videoUrl"),
|
||||
primaryButton: {
|
||||
label: getVal(prefix + "primaryLabel"),
|
||||
href: getVal(prefix + "primaryHref")
|
||||
},
|
||||
secondaryButton: {
|
||||
label: getVal(prefix + "secondaryLabel"),
|
||||
href: getVal(prefix + "secondaryHref")
|
||||
}
|
||||
};
|
||||
|
||||
// Bỏ qua slide trống hoàn toàn
|
||||
const hasContent = slide.title || slide.subtitle || slide.description || slide.heroImage || slide.videoUrl || slide.primaryButton.label || slide.secondaryButton.label;
|
||||
|
||||
if (hasContent) {
|
||||
slides.push(slide);
|
||||
}
|
||||
});
|
||||
|
||||
// Legacy single-slide fields: map từ slide đầu tiên (nếu có) để không vỡ FE cũ
|
||||
const first = slides[0] || {};
|
||||
|
||||
return {
|
||||
backgroundImage,
|
||||
slides,
|
||||
title: first.title || "",
|
||||
subtitle: first.subtitle || "",
|
||||
description: first.description || "",
|
||||
heroImage: first.heroImage || "",
|
||||
videoUrl: first.videoUrl || "",
|
||||
primaryButton: first.primaryButton || {
|
||||
label: "",
|
||||
href: ""
|
||||
},
|
||||
secondaryButton: first.secondaryButton || {
|
||||
label: "",
|
||||
href: ""
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Add / remove slides on the client
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const container = document.getElementById("heroSlidesContainer");
|
||||
const addBtn = document.getElementById("addHeroSlideBtn");
|
||||
|
||||
if (!container || !addBtn)
|
||||
return;
|
||||
|
||||
|
||||
// Re-index slides, update labels, IDs and upload target attributes
|
||||
const updateLabels = () => {
|
||||
container.querySelectorAll(".hero-slide-item").forEach((el, idx) => {
|
||||
el.setAttribute("data-index", String(idx));
|
||||
|
||||
const label = el.querySelector(".hero-slide-label");
|
||||
if (label)
|
||||
label.textContent = String(idx + 1);
|
||||
|
||||
|
||||
// Update input/textarea IDs to match new index
|
||||
el.querySelectorAll("input, textarea").forEach((input) => {
|
||||
if (!input.id)
|
||||
return;
|
||||
|
||||
const newId = input.id.replace(/heroSlide_\d+_/, `heroSlide_${idx}_`);
|
||||
input.id = newId;
|
||||
});
|
||||
|
||||
// Update upload button target-input so upload goes to correct slide
|
||||
el.querySelectorAll(".btn-upload-image").forEach((btn) => {
|
||||
const target = btn.getAttribute("data-target-input") || "";
|
||||
if (!target)
|
||||
return;
|
||||
|
||||
const newTarget = target.replace(/heroSlide_\d+_/, `heroSlide_${idx}_`);
|
||||
btn.setAttribute("data-target-input", newTarget);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
addBtn.addEventListener("click", () => {
|
||||
const template = container.querySelector(".hero-slide-item");
|
||||
if (!template)
|
||||
return;
|
||||
|
||||
|
||||
const clone = template.cloneNode(true);
|
||||
|
||||
// Clear values for the cloned slide (IDs will be fixed by updateLabels)
|
||||
clone.querySelectorAll("input, textarea").forEach((input) => {
|
||||
input.value = "";
|
||||
});
|
||||
|
||||
// Clear image previews
|
||||
clone.querySelectorAll("img").forEach((img) => {
|
||||
img.classList.add("d-none");
|
||||
img.removeAttribute("src");
|
||||
});
|
||||
|
||||
container.appendChild(clone);
|
||||
updateLabels();
|
||||
});
|
||||
|
||||
container.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest(".btn-remove-hero-slide");
|
||||
if (!btn)
|
||||
return;
|
||||
|
||||
|
||||
const card = btn.closest(".hero-slide-item");
|
||||
if (!card)
|
||||
return;
|
||||
|
||||
|
||||
const all = container.querySelectorAll(".hero-slide-item");
|
||||
if (all.length <= 1) { // Không cho xóa slide cuối cùng
|
||||
return;
|
||||
}
|
||||
|
||||
card.remove();
|
||||
updateLabels();
|
||||
});
|
||||
|
||||
// Initial normalization (in case indices rendered from server are not 0..n)
|
||||
updateLabels();
|
||||
});
|
||||
</script>
|
||||
@@ -13,49 +13,26 @@
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="testimonialsHeading"
|
||||
value="<%= data.testimonials?.heading || '' %>"
|
||||
placeholder="e.g., Student Reviews & Testimonials"
|
||||
/>
|
||||
<input type="text" class="form-control" id="testimonialsHeading"
|
||||
value="<%= data.testimonials?.heading || '' %>" placeholder="e.g., Student Reviews & Testimonials" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subheading</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="testimonialsSubheading"
|
||||
value="<%= data.testimonials?.subheading || '' %>"
|
||||
placeholder="e.g., What Our Students Say"
|
||||
/>
|
||||
<input type="text" class="form-control" id="testimonialsSubheading"
|
||||
value="<%= data.testimonials?.subheading || '' %>" placeholder="e.g., What Our Students Say" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Video URL</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="testimonialsVideoUrl"
|
||||
value="<%= data.testimonials?.videoUrl || '' %>"
|
||||
placeholder="https://www.youtube.com/watch?v=..."
|
||||
/>
|
||||
<input type="text" class="form-control" id="testimonialsVideoUrl"
|
||||
value="<%= data.testimonials?.videoUrl || '' %>" placeholder="https://www.youtube.com/watch?v=..." />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Video Thumbnail</label>
|
||||
<div class="input-group mb-2">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="testimonialsVideoThumbnail"
|
||||
value="<%= data.testimonials?.videoThumbnail || '' %>"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="testimonialsVideoThumbnail"
|
||||
data-image-type="home"
|
||||
>
|
||||
<input type="text" class="form-control" id="testimonialsVideoThumbnail"
|
||||
value="<%= data.testimonials?.videoThumbnail || '' %>" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="testimonialsVideoThumbnail" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
@@ -68,82 +45,59 @@
|
||||
<!-- Testimonial Items -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-comments me-2"></i>Testimonials
|
||||
</h6>
|
||||
<button type="button" class="btn btn-sm btn-primary" id="addTestimonialBtn">
|
||||
<i class="fas fa-plus me-1"></i>Add Testimonial
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body" id="testimonialsItemsContainer">
|
||||
<% (data.testimonials?.items || []).forEach(function(item, index) { %>
|
||||
<div class="card mb-3 bg-light border testimonial-item" data-index="<%= index %>">
|
||||
<div class="card-header d-flex justify-content-between align-items-center bg-white">
|
||||
<h6 class="card-title fw-bold mb-0">Testimonial <span class="testimonial-label">
|
||||
<%= index + 1 %>
|
||||
</span></h6>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger btn-remove-testimonial">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% (data.testimonials?.items || []).forEach(function(item, index) { %>
|
||||
<div class="card mb-3 bg-light border">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title fw-bold mb-3">Testimonial <%= index + 1 %></h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="testimonialsName_<%= index %>"
|
||||
value="<%= item.name || '' %>"
|
||||
placeholder="e.g., Sohel Tanvir"
|
||||
/>
|
||||
<input type="text" class="form-control" id="testimonialsName_<%= index %>"
|
||||
value="<%= item.name || '' %>" placeholder="e.g., Sohel Tanvir" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Role</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="testimonialsRole_<%= index %>"
|
||||
value="<%= item.role || '' %>"
|
||||
placeholder="e.g., Student"
|
||||
/>
|
||||
<input type="text" class="form-control" id="testimonialsRole_<%= index %>"
|
||||
value="<%= item.role || '' %>" placeholder="e.g., Student" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Country</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="testimonialsCountry_<%= index %>"
|
||||
value="<%= item.country || '' %>"
|
||||
placeholder="e.g., Canada"
|
||||
/>
|
||||
<input type="text" class="form-control" id="testimonialsCountry_<%= index %>"
|
||||
value="<%= item.country || '' %>" placeholder="e.g., Canada" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Rating</label>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
id="testimonialsRating_<%= index %>"
|
||||
value="<%= item.rating || 5 %>"
|
||||
min="1"
|
||||
max="5"
|
||||
/>
|
||||
<input type="number" class="form-control" id="testimonialsRating_<%= index %>"
|
||||
value="<%= item.rating || 5 %>" min="1" max="5" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Comment</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="testimonialsComment_<%= index %>"
|
||||
rows="3"
|
||||
placeholder="Enter testimonial comment"
|
||||
><%= item.comment || '' %></textarea>
|
||||
<textarea class="form-control" id="testimonialsComment_<%= index %>" rows="3"
|
||||
placeholder="Enter testimonial comment"><%= item.comment || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Avatar</label>
|
||||
<div class="input-group mb-2">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="testimonialsAvatar_<%= index %>"
|
||||
value="<%= item.avatar || '' %>"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="testimonialsAvatar_<%= index %>"
|
||||
data-image-type="home"
|
||||
>
|
||||
<input type="text" class="form-control" id="testimonialsAvatar_<%= index %>"
|
||||
value="<%= item.avatar || '' %>" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="testimonialsAvatar_<%= index %>" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
@@ -157,3 +111,101 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Register scraper to collect Testimonials data before form submit
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
window.homeScrapers.testimonials = () => {
|
||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||
|
||||
const items = [];
|
||||
document.querySelectorAll(".testimonial-item").forEach((el, idx) => {
|
||||
const index = el.getAttribute("data-index") || idx;
|
||||
|
||||
const name = getVal(`testimonialsName_${index}`);
|
||||
const role = getVal(`testimonialsRole_${index}`);
|
||||
const country = getVal(`testimonialsCountry_${index}`);
|
||||
const ratingStr = getVal(`testimonialsRating_${index}`);
|
||||
const rating = ratingStr ? parseInt(ratingStr, 10) : 5;
|
||||
const comment = getVal(`testimonialsComment_${index}`);
|
||||
const avatar = getVal(`testimonialsAvatar_${index}`);
|
||||
|
||||
if (name || role || country || comment || avatar) {
|
||||
items.push({
|
||||
name,
|
||||
role,
|
||||
country,
|
||||
rating: isNaN(rating) ? 5 : Math.min(Math.max(rating, 1), 5),
|
||||
comment,
|
||||
avatar,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
heading: getVal("testimonialsHeading"),
|
||||
subheading: getVal("testimonialsSubheading"),
|
||||
videoUrl: getVal("testimonialsVideoUrl"),
|
||||
videoThumbnail: getVal("testimonialsVideoThumbnail"),
|
||||
items,
|
||||
};
|
||||
};
|
||||
|
||||
// Client-side add/remove for testimonials
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const container = document.getElementById("testimonialsItemsContainer");
|
||||
const addBtn = document.getElementById("addTestimonialBtn");
|
||||
|
||||
if (!container || !addBtn) return;
|
||||
|
||||
const updateLabels = () => {
|
||||
container.querySelectorAll(".testimonial-item").forEach((el, idx) => {
|
||||
el.setAttribute("data-index", String(idx));
|
||||
|
||||
const label = el.querySelector(".testimonial-label");
|
||||
if (label) label.textContent = String(idx + 1);
|
||||
|
||||
el.querySelectorAll("input, textarea").forEach((input) => {
|
||||
if (!input.id) return;
|
||||
const newId = input.id.replace(/testimonials\w+_\d+/, (match) => {
|
||||
const [prefix] = match.split("_");
|
||||
return `${prefix}_${idx}`;
|
||||
});
|
||||
input.id = newId;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
addBtn.addEventListener("click", () => {
|
||||
const template = container.querySelector(".testimonial-item");
|
||||
if (!template) return;
|
||||
|
||||
const clone = template.cloneNode(true);
|
||||
|
||||
clone.querySelectorAll("input, textarea").forEach((input) => {
|
||||
input.value = "";
|
||||
});
|
||||
|
||||
container.appendChild(clone);
|
||||
updateLabels();
|
||||
});
|
||||
|
||||
container.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest(".btn-remove-testimonial");
|
||||
if (!btn) return;
|
||||
|
||||
const card = btn.closest(".testimonial-item");
|
||||
if (!card) return;
|
||||
|
||||
const all = container.querySelectorAll(".testimonial-item");
|
||||
if (all.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
card.remove();
|
||||
updateLabels();
|
||||
});
|
||||
|
||||
updateLabels();
|
||||
});
|
||||
</script>
|
||||
@@ -12,50 +12,28 @@
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="videoGalleryHeading"
|
||||
value="<%= data.videoGallery?.heading || '' %>"
|
||||
placeholder="e.g., VIDEO PLAY GALLERY"
|
||||
/>
|
||||
<input type="text" class="form-control" id="videoGalleryHeading"
|
||||
value="<%= data.videoGallery?.heading || '' %>" placeholder="e.g., VIDEO PLAY GALLERY" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Video URL</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="videoGalleryVideoUrl"
|
||||
value="<%= data.videoGallery?.videoUrl || '' %>"
|
||||
placeholder="https://example.com/video.mp4"
|
||||
/>
|
||||
<input type="text" class="form-control" id="videoGalleryVideoUrl"
|
||||
value="<%= data.videoGallery?.videoUrl || '' %>" placeholder="https://example.com/video.mp4" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Thumbnail Image</label>
|
||||
<div class="input-group mb-2">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="videoGalleryThumbnail"
|
||||
value="<%= data.videoGallery?.thumbnail || '' %>"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="videoGalleryThumbnail"
|
||||
data-image-type="home"
|
||||
>
|
||||
<input type="text" class="form-control" id="videoGalleryThumbnail"
|
||||
value="<%= data.videoGallery?.thumbnail || '' %>" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="videoGalleryThumbnail" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<% if (data.videoGallery?.thumbnail) { %>
|
||||
<div class="mt-2">
|
||||
<img
|
||||
src="<%= data.videoGallery.thumbnail %>"
|
||||
class="img-thumbnail"
|
||||
style="height: 200px; width: 100%; object-fit: cover;"
|
||||
alt="Thumbnail preview"
|
||||
/>
|
||||
<img src="<%= data.videoGallery.thumbnail %>" class="img-thumbnail"
|
||||
style="height: 200px; width: 100%; object-fit: cover;" alt="Thumbnail preview" />
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
@@ -65,3 +43,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Register scraper to collect Video Gallery data before form submit
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
window.homeScrapers.videoGallery = () => {
|
||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||
|
||||
return {
|
||||
heading: getVal("videoGalleryHeading"),
|
||||
videoUrl: getVal("videoGalleryVideoUrl"),
|
||||
thumbnail: getVal("videoGalleryThumbnail"),
|
||||
};
|
||||
};
|
||||
</script>
|
||||
@@ -13,114 +13,76 @@
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="visaCountriesHeading"
|
||||
value="<%= data.visaCountries?.heading || '' %>"
|
||||
placeholder="e.g., Visa & VISAWAY Services To UK"
|
||||
/>
|
||||
<input type="text" class="form-control" id="visaCountriesHeading"
|
||||
value="<%= data.visaCountries?.heading || '' %>" placeholder="e.g., Visa & VISAWAY Services To UK" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subheading</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="visaCountriesSubheading"
|
||||
value="<%= data.visaCountries?.subheading || '' %>"
|
||||
placeholder="e.g., UK. United Kingdom"
|
||||
/>
|
||||
<input type="text" class="form-control" id="visaCountriesSubheading"
|
||||
value="<%= data.visaCountries?.subheading || '' %>" placeholder="e.g., UK. United Kingdom" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="visaCountriesDescription"
|
||||
rows="3"
|
||||
placeholder="Enter description"
|
||||
><%= data.visaCountries?.description || '' %></textarea>
|
||||
<textarea class="form-control" id="visaCountriesDescription" rows="3"
|
||||
placeholder="Enter description"><%= data.visaCountries?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Countries -->
|
||||
<!-- Countries (Featured Country configuration) -->
|
||||
<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-globe me-2"></i>Countries
|
||||
<i class="fas fa-globe me-2"></i>Featured Country
|
||||
</h6>
|
||||
<small class="text-muted">This country is used in the home page feature section.</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% (data.visaCountries?.countries || []).forEach(function(country, index) { %>
|
||||
<div class="card mb-3 bg-light border">
|
||||
<% const featured=(data.visaCountries && Array.isArray(data.visaCountries.countries) &&
|
||||
data.visaCountries.countries.length> 0)
|
||||
? data.visaCountries.countries[0]
|
||||
: {};
|
||||
%>
|
||||
<div class="card mb-3 bg-light border visa-country-item" data-index="0">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title fw-bold mb-3">Country <%= index + 1 %></h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Country Name</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="visaCountriesName_<%= index %>"
|
||||
value="<%= country.name || '' %>"
|
||||
placeholder="e.g., United Kingdom"
|
||||
/>
|
||||
<input type="text" class="form-control" id="visaCountriesName_0" value="<%= featured.name || '' %>"
|
||||
placeholder="e.g., United Kingdom" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Country Code</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="visaCountriesCode_<%= index %>"
|
||||
value="<%= country.code || '' %>"
|
||||
placeholder="e.g., UK"
|
||||
/>
|
||||
<input type="text" class="form-control" id="visaCountriesCode_0" value="<%= featured.code || '' %>"
|
||||
placeholder="e.g., UK" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Flag Image</label>
|
||||
<label class="form-label fw-medium">Flag / Illustration Image</label>
|
||||
<div class="input-group mb-2">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="visaCountriesFlag_<%= index %>"
|
||||
value="<%= country.flag || '' %>"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="visaCountriesFlag_<%= index %>"
|
||||
data-image-type="home"
|
||||
>
|
||||
<input type="text" class="form-control" id="visaCountriesFlag_0"
|
||||
value="<%= featured.flag || '' %>" placeholder="/assets/img/home-1/feature/shape.png" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="visaCountriesFlag_0" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="visaCountriesLink_<%= index %>"
|
||||
value="<%= country.link || '' %>"
|
||||
placeholder="/country-details/uk"
|
||||
/>
|
||||
<input type="text" class="form-control" id="visaCountriesLink_0" value="<%= featured.link || '' %>"
|
||||
placeholder="/country-details/uk" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Visa Types (comma-separated)</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="visaCountriesVisaTypes_<%= index %>"
|
||||
rows="2"
|
||||
placeholder="e.g., Student Visa, Work Visa, Tourist Visa"
|
||||
><%= (country.visaTypes || []).join(', ') %></textarea>
|
||||
<textarea class="form-control" id="visaCountriesVisaTypes_0" rows="2"
|
||||
placeholder="e.g., Student Visa, Work Visa, Tourist Visa"><%= (featured.visaTypes || []).join(', ') %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,23 +99,13 @@
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Label</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="visaCountriesCtaLabel"
|
||||
value="<%= data.visaCountries?.ctaButton?.label || '' %>"
|
||||
placeholder="e.g., Get Started"
|
||||
/>
|
||||
<input type="text" class="form-control" id="visaCountriesCtaLabel"
|
||||
value="<%= data.visaCountries?.ctaButton?.label || '' %>" placeholder="e.g., Get Started" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="visaCountriesCtaHref"
|
||||
value="<%= data.visaCountries?.ctaButton?.href || '' %>"
|
||||
placeholder="/contact"
|
||||
/>
|
||||
<input type="text" class="form-control" id="visaCountriesCtaHref"
|
||||
value="<%= data.visaCountries?.ctaButton?.href || '' %>" placeholder="/contact" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -161,3 +113,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Register scraper to collect Visa Countries data before form submit
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
window.homeScrapers.visaCountries = () => {
|
||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||
|
||||
const visaTypesRaw = getVal("visaCountriesVisaTypes_0");
|
||||
const visaTypes = visaTypesRaw ? visaTypesRaw.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
||||
|
||||
const featuredCountry = {
|
||||
name: getVal("visaCountriesName_0"),
|
||||
code: getVal("visaCountriesCode_0"),
|
||||
flag: getVal("visaCountriesFlag_0"),
|
||||
link: getVal("visaCountriesLink_0"),
|
||||
visaTypes
|
||||
};
|
||||
|
||||
return {
|
||||
heading: getVal("visaCountriesHeading"),
|
||||
subheading: getVal("visaCountriesSubheading"),
|
||||
description: getVal("visaCountriesDescription"),
|
||||
countries: [featuredCountry],
|
||||
ctaButton: {
|
||||
label: getVal("visaCountriesCtaLabel"),
|
||||
href: getVal("visaCountriesCtaHref")
|
||||
}
|
||||
};
|
||||
};
|
||||
</script>
|
||||
@@ -13,23 +13,13 @@
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="visaSolutionsHeading"
|
||||
value="<%= data.visaSolutions?.heading || '' %>"
|
||||
placeholder="e.g., Comprehensive Visa Solutions"
|
||||
/>
|
||||
<input type="text" class="form-control" id="visaSolutionsHeading"
|
||||
value="<%= data.visaSolutions?.heading || '' %>" placeholder="e.g., Comprehensive Visa Solutions" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subheading</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="visaSolutionsSubheading"
|
||||
value="<%= data.visaSolutions?.subheading || '' %>"
|
||||
placeholder="e.g., Our Expert Services"
|
||||
/>
|
||||
<input type="text" class="form-control" id="visaSolutionsSubheading"
|
||||
value="<%= data.visaSolutions?.subheading || '' %>" placeholder="e.g., Our Expert Services" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,55 +29,46 @@
|
||||
<!-- Services Items -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-list-ul me-2"></i>Visa Solutions Items
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body" id="visaSolutionsItemsContainer">
|
||||
<% const vsItems=(data.visaSolutions && Array.isArray(data.visaSolutions.items) &&
|
||||
data.visaSolutions.items.length===4) ? data.visaSolutions.items : (data.visaSolutions &&
|
||||
Array.isArray(data.visaSolutions.items) && data.visaSolutions.items.length> 0)
|
||||
? (function () {
|
||||
const clone = data.visaSolutions.items.slice(0, 4);
|
||||
while (clone.length < 4) clone.push({}); return clone; })() : [{}, {}, {}, {}]; %>
|
||||
<% vsItems.forEach(function(item, index) { %>
|
||||
<div class="card mb-3 bg-light border visa-solution-item" data-index="<%= index %>">
|
||||
<div class="card-header d-flex justify-content-between align-items-center bg-white">
|
||||
<h6 class="card-title fw-bold mb-0">Service <span class="visa-solution-label">
|
||||
<%= index + 1 %>
|
||||
</span></h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% (data.visaSolutions?.items || []).forEach(function(item, index) { %>
|
||||
<div class="card mb-3 bg-light border">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title fw-bold mb-3">Service <%= index + 1 %></h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-medium">Number</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="visaSolutionsNumber_<%= index %>"
|
||||
value="<%= item.number || '' %>"
|
||||
placeholder="e.g., 01"
|
||||
/>
|
||||
<input type="text" class="form-control" id="visaSolutionsNumber_<%= index %>"
|
||||
value="<%= item.number || '' %>" placeholder="e.g., 01" />
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="visaSolutionsTitle_<%= index %>"
|
||||
value="<%= item.title || '' %>"
|
||||
placeholder="e.g., Student Visa Guidance"
|
||||
/>
|
||||
<input type="text" class="form-control" id="visaSolutionsTitle_<%= index %>"
|
||||
value="<%= item.title || '' %>" placeholder="e.g., Student Visa Guidance" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="visaSolutionsDescription_<%= index %>"
|
||||
rows="2"
|
||||
placeholder="Enter description"
|
||||
><%= item.description || '' %></textarea>
|
||||
<textarea class="form-control" id="visaSolutionsDescription_<%= index %>" rows="2"
|
||||
placeholder="Enter description"><%= item.description || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="visaSolutionsLink_<%= index %>"
|
||||
value="<%= item.link || '' %>"
|
||||
placeholder="/service-details"
|
||||
/>
|
||||
<input type="text" class="form-control" id="visaSolutionsLink_<%= index %>"
|
||||
value="<%= item.link || '' %>" placeholder="/service-details" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,3 +79,59 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Register scraper to collect Visa Solutions data before form submit
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
window.homeScrapers.visaSolutions = () => {
|
||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||
|
||||
const items = [];
|
||||
document.querySelectorAll(".visa-solution-item").forEach((el, idx) => {
|
||||
const index = el.getAttribute("data-index") || idx;
|
||||
|
||||
const number = getVal(`visaSolutionsNumber_${index}`);
|
||||
const title = getVal(`visaSolutionsTitle_${index}`);
|
||||
const description = getVal(`visaSolutionsDescription_${index}`);
|
||||
const link = getVal(`visaSolutionsLink_${index}`);
|
||||
|
||||
if (number || title || description || link) {
|
||||
items.push({ number, title, description, link });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
heading: getVal("visaSolutionsHeading"),
|
||||
subheading: getVal("visaSolutionsSubheading"),
|
||||
items,
|
||||
};
|
||||
};
|
||||
|
||||
// Client-side add/remove for visa solutions
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const container = document.getElementById("visaSolutionsItemsContainer");
|
||||
if (!container) return;
|
||||
|
||||
const updateLabels = () => {
|
||||
const items = container.querySelectorAll(".visa-solution-item");
|
||||
|
||||
items.forEach((el, idx) => {
|
||||
el.setAttribute("data-index", String(idx));
|
||||
|
||||
const label = el.querySelector(".visa-solution-label");
|
||||
if (label) label.textContent = String(idx + 1);
|
||||
|
||||
el.querySelectorAll("input, textarea").forEach((input) => {
|
||||
if (!input.id) return;
|
||||
const newId = input.id.replace(/visaSolutions\w+_\d+/, (match) => {
|
||||
const [prefix] = match.split("_");
|
||||
return `${prefix}_${idx}`;
|
||||
});
|
||||
input.id = newId;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
updateLabels();
|
||||
});
|
||||
</script>
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-4">
|
||||
<!-- Basic Info -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card border shadow-sm mb-3">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||
@@ -13,32 +13,78 @@
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="whyChooseUsHeading"
|
||||
<input type="text" class="form-control" id="whyChooseUsHeading"
|
||||
value="<%= data.whyChooseUs?.heading || '' %>"
|
||||
placeholder="e.g., Turning Study Abroad Dreams Into Reality"
|
||||
/>
|
||||
placeholder="e.g., Turning Study Abroad Dreams Into Reality" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subheading</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="whyChooseUsSubheading"
|
||||
value="<%= data.whyChooseUs?.subheading || '' %>"
|
||||
placeholder="e.g., About Our Consultancy"
|
||||
/>
|
||||
<input type="text" class="form-control" id="whyChooseUsSubheading"
|
||||
value="<%= data.whyChooseUs?.subheading || '' %>" placeholder="e.g., About Our Consultancy" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Highlight Word (Optional)</label>
|
||||
<input type="text" class="form-control" id="whyChooseUsHighlightWord"
|
||||
value="<%= data.whyChooseUs?.highlightWord || '' %>" placeholder="e.g., Dreams" />
|
||||
<small class="text-muted">This word in the heading will be wrapped in a colored span.</small>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="whyChooseUsDescription"
|
||||
rows="3"
|
||||
placeholder="Enter description"
|
||||
><%= data.whyChooseUs?.description || '' %></textarea>
|
||||
<textarea class="form-control" id="whyChooseUsDescription" rows="3"
|
||||
placeholder="Enter description"><%= data.whyChooseUs?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- About Images -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm mb-3">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-images me-2"></i>About Images (Left side)
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Main Image</label>
|
||||
<small class="text-muted d-block mb-1">Recommended size: 375x419px</small>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="whyChooseUsMainImage"
|
||||
value="<%= data.whyChooseUs?.mainImage || '' %>" placeholder="/assets/img/home-1/about/about-1.jpg" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="whyChooseUsMainImage" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<% if (data.whyChooseUs?.mainImage) { %>
|
||||
<div class="mt-2">
|
||||
<img src="<%= data.whyChooseUs.mainImage %>" class="img-thumbnail"
|
||||
style="height: 200px; width: 100%; object-fit: cover;" alt="Main about image preview" />
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Secondary Image</label>
|
||||
<small class="text-muted d-block mb-1">Recommended size: 376x394px</small>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="whyChooseUsSecondaryImage"
|
||||
value="<%= data.whyChooseUs?.secondaryImage || '' %>"
|
||||
placeholder="/assets/img/home-1/about/about-02.jpg" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="whyChooseUsSecondaryImage" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<% if (data.whyChooseUs?.secondaryImage) { %>
|
||||
<div class="mt-2">
|
||||
<img src="<%= data.whyChooseUs.secondaryImage %>" class="img-thumbnail"
|
||||
style="height: 200px; width: 100%; object-fit: cover;" alt="Secondary about image preview" />
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,46 +103,29 @@
|
||||
<% (data.whyChooseUs?.items || []).forEach(function(item, index) { %>
|
||||
<div class="card mb-3 bg-light border">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title fw-bold mb-3">Item <%= index + 1 %></h6>
|
||||
<h6 class="card-title fw-bold mb-3">Item <%= index + 1 %>
|
||||
</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Icon URL</label>
|
||||
<div class="input-group mb-2">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="whyChooseUsIcon_<%= index %>"
|
||||
value="<%= item.icon || '' %>"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="whyChooseUsIcon_<%= index %>"
|
||||
data-image-type="home"
|
||||
>
|
||||
<input type="text" class="form-control" id="whyChooseUsIcon_<%= index %>"
|
||||
value="<%= item.icon || '' %>" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="whyChooseUsIcon_<%= index %>" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="whyChooseUsTitle_<%= index %>"
|
||||
value="<%= item.title || '' %>"
|
||||
placeholder="e.g., Global Reach"
|
||||
/>
|
||||
<input type="text" class="form-control" id="whyChooseUsTitle_<%= index %>"
|
||||
value="<%= item.title || '' %>" placeholder="e.g., Global Reach" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="whyChooseUsItemDescription_<%= index %>"
|
||||
value="<%= item.description || '' %>"
|
||||
placeholder="e.g., Expanding Opportunities Worldwide"
|
||||
/>
|
||||
<input type="text" class="form-control" id="whyChooseUsItemDescription_<%= index %>"
|
||||
value="<%= item.description || '' %>" placeholder="e.g., Expanding Opportunities Worldwide" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,13 +147,8 @@
|
||||
<% (data.whyChooseUs?.features || []).forEach(function(feature, index) { %>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">Feature <%= index + 1 %></label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="whyChooseUsFeature_<%= index %>"
|
||||
value="<%= feature %>"
|
||||
placeholder="Enter feature"
|
||||
/>
|
||||
<input type="text" class="form-control" id="whyChooseUsFeature_<%= index %>" value="<%= feature %>"
|
||||
placeholder="Enter feature" />
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
@@ -143,23 +167,13 @@
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Label</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="whyChooseUsCtaLabel"
|
||||
value="<%= data.whyChooseUs?.ctaButton?.label || '' %>"
|
||||
placeholder="e.g., Get Started"
|
||||
/>
|
||||
<input type="text" class="form-control" id="whyChooseUsCtaLabel"
|
||||
value="<%= data.whyChooseUs?.ctaButton?.label || '' %>" placeholder="e.g., Get Started" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="whyChooseUsCtaHref"
|
||||
value="<%= data.whyChooseUs?.ctaButton?.href || '' %>"
|
||||
placeholder="/about"
|
||||
/>
|
||||
<input type="text" class="form-control" id="whyChooseUsCtaHref"
|
||||
value="<%= data.whyChooseUs?.ctaButton?.href || '' %>" placeholder="/about" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -167,3 +181,53 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Register scraper to collect Why Choose Us data before form submit
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
window.homeScrapers.whyChooseUs = () => {
|
||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||
|
||||
// Collect items
|
||||
const items = [];
|
||||
document.querySelectorAll('input[id^="whyChooseUsTitle_"]').forEach((titleInput) => {
|
||||
const id = titleInput.id || "";
|
||||
const match = id.match(/whyChooseUsTitle_(\d+)/);
|
||||
if (!match) return;
|
||||
const idx = match[1];
|
||||
|
||||
const title = (titleInput.value || "").trim();
|
||||
const icon = getVal(`whyChooseUsIcon_${idx}`);
|
||||
const description = getVal(`whyChooseUsItemDescription_${idx}`);
|
||||
|
||||
// Bỏ qua item trống hoàn toàn
|
||||
if (title || icon || description) {
|
||||
items.push({ icon, title, description });
|
||||
}
|
||||
});
|
||||
|
||||
// Collect features
|
||||
const features = [];
|
||||
document.querySelectorAll('input[id^="whyChooseUsFeature_"]').forEach((featureInput) => {
|
||||
const value = (featureInput.value || "").trim();
|
||||
if (value) {
|
||||
features.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
heading: getVal("whyChooseUsHeading"),
|
||||
subheading: getVal("whyChooseUsSubheading"),
|
||||
description: getVal("whyChooseUsDescription"),
|
||||
highlightWord: getVal("whyChooseUsHighlightWord"),
|
||||
mainImage: getVal("whyChooseUsMainImage"),
|
||||
secondaryImage: getVal("whyChooseUsSecondaryImage"),
|
||||
items,
|
||||
features,
|
||||
ctaButton: {
|
||||
label: getVal("whyChooseUsCtaLabel"),
|
||||
href: getVal("whyChooseUsCtaHref"),
|
||||
},
|
||||
};
|
||||
};
|
||||
</script>
|
||||
@@ -362,6 +362,19 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Image Contact</label>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" id="contact_image_input" name="contact_image" class="form-control" placeholder="/uploads/visa/contact-image.jpg" required />
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="document.getElementById('fileContactImageInput').click()">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
<input type="file" id="fileContactImageInput" style="display:none" accept="image/*"/>
|
||||
<div style="margin-top: 10px;">
|
||||
<img id="preview_contact_image" src="" alt="Contact Image Preview" style="max-width: 200px; max-height: 150px; object-fit: cover; display: none; border-radius: 4px; border: 1px solid #ddd;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Section Title</label>
|
||||
<input type="text" name="contact_sectionTitle" class="form-control" placeholder="e.g. Visa & Immigration" required/>
|
||||
@@ -452,18 +465,20 @@
|
||||
|
||||
// Image upload handler
|
||||
function setupImageUploadHandlers() {
|
||||
const imageInputs = ["fileFlagInput", "fileDetailMainInput", "fileGalleryInput1", "fileGalleryInput2"];
|
||||
const imageInputs = ["fileFlagInput", "fileDetailMainInput", "fileGalleryInput1", "fileGalleryInput2", "fileContactImageInput"];
|
||||
const previewMap = {
|
||||
fileFlagInput: "preview_icon",
|
||||
fileDetailMainInput: "preview_main_detail",
|
||||
fileGalleryInput1: "preview_bannerImageGallery1",
|
||||
fileGalleryInput2: "preview_bannerImageGallery2"
|
||||
fileGalleryInput2: "preview_bannerImageGallery2",
|
||||
fileContactImageInput: "preview_contact_image"
|
||||
};
|
||||
const inputMap = {
|
||||
fileFlagInput: "icon_input",
|
||||
fileDetailMainInput: "mainImage_detail",
|
||||
fileGalleryInput1: "bannerImageGallery1",
|
||||
fileGalleryInput2: "bannerImageGallery2"
|
||||
fileGalleryInput2: "bannerImageGallery2",
|
||||
fileContactImageInput: "contact_image_input"
|
||||
};
|
||||
|
||||
imageInputs.forEach((fileId) => {
|
||||
@@ -758,6 +773,17 @@
|
||||
|
||||
const contactInfo = country.detailedView?.contactInfo;
|
||||
if (contactInfo) {
|
||||
document.querySelector('input[name="contact_image"]').value = contactInfo.img || "";
|
||||
|
||||
// Update contact image preview
|
||||
if (contactInfo.img) {
|
||||
const contactPreview = document.getElementById("preview_contact_image");
|
||||
if (contactPreview) {
|
||||
contactPreview.src = contactInfo.img;
|
||||
contactPreview.style.display = "block";
|
||||
}
|
||||
}
|
||||
document.querySelector('input[name="contact_image"]').value = contactInfo.img || "";
|
||||
document.querySelector('input[name="contact_sectionTitle"]').value = contactInfo.sectionTitle || "";
|
||||
document.querySelector('input[name="contact_helpText"]').value = contactInfo.helpText || "";
|
||||
document.querySelector('input[name="contact_phone_label"]').value = contactInfo.phone?.label || "";
|
||||
@@ -766,6 +792,7 @@
|
||||
document.querySelector('input[name="contact_email"]').value = contactInfo.email?.value || "";
|
||||
document.querySelector('input[name="contact_location_label"]').value = contactInfo.location?.label || "";
|
||||
document.querySelector('input[name="contact_location"]').value = contactInfo.location?.address || "";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -843,6 +870,7 @@
|
||||
document.getElementById("preview_main_detail").style.display = "none";
|
||||
document.getElementById("preview_bannerImageGallery1").style.display = "none";
|
||||
document.getElementById("preview_bannerImageGallery2").style.display = "none";
|
||||
document.getElementById("preview_contact_image").style.display = "none";
|
||||
|
||||
// Clear related countries previews
|
||||
for (let i = 0; i < 7; i++) {
|
||||
@@ -858,12 +886,13 @@
|
||||
relatedNameInputs.forEach(input => input.value = "");
|
||||
relatedIconInputs.forEach(input => input.value = "");
|
||||
|
||||
const contactFields = ["contact_sectionTitle", "contact_helpText", "contact_phone_label", "contact_phone", "contact_email_label", "contact_email", "contact_location_label", "contact_location"];
|
||||
const contactFields = ["contact_image", "contact_sectionTitle", "contact_helpText", "contact_phone_label", "contact_phone", "contact_email_label", "contact_email", "contact_location_label", "contact_location"];
|
||||
contactFields.forEach(fieldName => {
|
||||
const field = document.querySelector(`input[name="${fieldName}"]`);
|
||||
if (field) field.value = "";
|
||||
});
|
||||
|
||||
|
||||
showFormView();
|
||||
});
|
||||
|
||||
@@ -954,6 +983,7 @@
|
||||
icon: document.getElementById(`related_url_${index}`)?.value || ""
|
||||
})),
|
||||
contactInfo: {
|
||||
img: document.querySelector('input[name="contact_image"]').value,
|
||||
sectionTitle: document.querySelector('input[name="contact_sectionTitle"]').value,
|
||||
helpText: document.querySelector('input[name="contact_helpText"]').value,
|
||||
phone: {
|
||||
@@ -971,6 +1001,7 @@
|
||||
}
|
||||
}
|
||||
};
|
||||
console.log('Payload:', payload);
|
||||
|
||||
try {
|
||||
btnSave.disabled = true;
|
||||
|
||||
@@ -60,9 +60,6 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/footer">Footer</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/about">About</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/about-us">About Us</a>
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user