forked from UKSOURCE/cms.hailearning.edu.vn
Merge remote-tracking branch 'origin/fea/nhat-06042026-menu-management' into fea/dat-11042026-merge-code-fix
This commit is contained in:
BIN
.env.example
BIN
.env.example
Binary file not shown.
@@ -445,14 +445,11 @@ exports.apiGetBlogs = async (req, res) => {
|
|||||||
};
|
};
|
||||||
exports.api = async (req, res) => {
|
exports.api = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const docs = await getAllHomeDocs();
|
// Chỉ dùng doc mới nhất, không merge nhiều docs
|
||||||
let data = docs[0]?.toObject() || {};
|
const doc = await getHomeDoc();
|
||||||
|
let data = doc?.toObject() || {};
|
||||||
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
||||||
|
|
||||||
if (docs.length > 1) {
|
|
||||||
data.hero = getPreferredHeroData(docs.map((doc) => doc.toObject()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Xử lý Blog Preview động ===
|
// === Xử lý Blog Preview động ===
|
||||||
const blogPreview = data.blogPreview || {};
|
const blogPreview = data.blogPreview || {};
|
||||||
let blogs = [];
|
let blogs = [];
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const { getServiceData } = require("../services/service.service");
|
const { getServiceData } = require("../services/service.service");
|
||||||
const Service = require("../models/service");
|
const Service = require("../models/service");
|
||||||
|
const syncServiceMenu = require("../services/syncServiceMenu");
|
||||||
const { addBaseUrlToImages, getFullImageUrl } = require("../utils/imageHelper");
|
const { addBaseUrlToImages, getFullImageUrl } = require("../utils/imageHelper");
|
||||||
const writeAuditLog = require("../audit/writeAuditLog");
|
const writeAuditLog = require("../audit/writeAuditLog");
|
||||||
const diffObject = require("../audit/diffObject");
|
const diffObject = require("../audit/diffObject");
|
||||||
@@ -98,6 +99,8 @@ exports.updateService = async (req, res) => {
|
|||||||
changes,
|
changes,
|
||||||
req,
|
req,
|
||||||
});
|
});
|
||||||
|
// Sync header menu children to reflect updated service name/slug
|
||||||
|
await syncServiceMenu(updatedData.services?.items || []);
|
||||||
req.flash("success_msg", "Service updated successfully");
|
req.flash("success_msg", "Service updated successfully");
|
||||||
res.redirect("/admin/service");
|
res.redirect("/admin/service");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -168,6 +171,9 @@ exports.update = async (req, res) => {
|
|||||||
await Service.create(updatedData);
|
await Service.create(updatedData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync header menu children to reflect current service list
|
||||||
|
await syncServiceMenu(updatedData.services?.items || []);
|
||||||
|
|
||||||
req.flash("success_msg", "Service updated successfully");
|
req.flash("success_msg", "Service updated successfully");
|
||||||
res.redirect("/admin/service");
|
res.redirect("/admin/service");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ const HeroSlideSchema = new Schema(
|
|||||||
|
|
||||||
const HeroSchema = new Schema(
|
const HeroSchema = new Schema(
|
||||||
{
|
{
|
||||||
|
// Toggle visibility on frontend
|
||||||
|
enabled: { type: Boolean, default: true },
|
||||||
|
|
||||||
// Background for whole hero section
|
// Background for whole hero section
|
||||||
backgroundImage: { type: String, default: "", maxlength: 255 },
|
backgroundImage: { type: String, default: "", maxlength: 255 },
|
||||||
|
|
||||||
@@ -101,12 +104,15 @@ const WhyChooseUsItemSchema = new Schema(
|
|||||||
|
|
||||||
const WhyChooseUsSchema = new Schema(
|
const WhyChooseUsSchema = new Schema(
|
||||||
{
|
{
|
||||||
heading: { type: String, default: "", maxlength: 72 },
|
// Toggle visibility on frontend
|
||||||
subheading: { type: String, default: "", maxlength: 48 },
|
enabled: { type: Boolean, default: true },
|
||||||
description: { type: String, default: "", maxlength: 260 },
|
|
||||||
highlightWord: { type: String, default: "", maxlength: 24 },
|
heading: { type: String, default: "" },
|
||||||
mainImage: { type: String, default: "", maxlength: 255 },
|
subheading: { type: String, default: "" },
|
||||||
secondaryImage: { type: String, default: "", maxlength: 255 },
|
description: { type: String, default: "" },
|
||||||
|
highlightWord: { type: String, default: "" },
|
||||||
|
mainImage: { type: String, default: "" },
|
||||||
|
secondaryImage: { type: String, default: "" },
|
||||||
items: { type: [WhyChooseUsItemSchema], default: [] },
|
items: { type: [WhyChooseUsItemSchema], default: [] },
|
||||||
features: { type: [{ type: String, maxlength: 96 }], default: [] },
|
features: { type: [{ type: String, maxlength: 96 }], default: [] },
|
||||||
ctaButton: { type: LinkSchema, default: () => ({}) },
|
ctaButton: { type: LinkSchema, default: () => ({}) },
|
||||||
@@ -126,8 +132,11 @@ const VisaSolutionItemSchema = new Schema(
|
|||||||
|
|
||||||
const VisaSolutionsSchema = new Schema(
|
const VisaSolutionsSchema = new Schema(
|
||||||
{
|
{
|
||||||
heading: { type: String, default: "", maxlength: 64 },
|
// Toggle visibility on frontend
|
||||||
subheading: { type: String, default: "", maxlength: 40 },
|
enabled: { type: Boolean, default: true },
|
||||||
|
|
||||||
|
heading: { type: String, default: "" },
|
||||||
|
subheading: { type: String, default: "" },
|
||||||
items: { type: [VisaSolutionItemSchema], default: [] },
|
items: { type: [VisaSolutionItemSchema], default: [] },
|
||||||
},
|
},
|
||||||
{ _id: false },
|
{ _id: false },
|
||||||
@@ -146,9 +155,12 @@ const VisaCountrySchema = new Schema(
|
|||||||
|
|
||||||
const VisaCountriesSchema = new Schema(
|
const VisaCountriesSchema = new Schema(
|
||||||
{
|
{
|
||||||
heading: { type: String, default: "", maxlength: 88 },
|
// Toggle visibility on frontend
|
||||||
subheading: { type: String, default: "", maxlength: 56 },
|
enabled: { type: Boolean, default: true },
|
||||||
description: { type: String, default: "", maxlength: 240 },
|
|
||||||
|
heading: { type: String, default: "" },
|
||||||
|
subheading: { type: String, default: "" },
|
||||||
|
description: { type: String, default: "" },
|
||||||
countries: { type: [VisaCountrySchema], default: [] },
|
countries: { type: [VisaCountrySchema], default: [] },
|
||||||
ctaButton: { type: LinkSchema, default: () => ({}) },
|
ctaButton: { type: LinkSchema, default: () => ({}) },
|
||||||
},
|
},
|
||||||
@@ -169,10 +181,13 @@ const TestimonialSchema = new Schema(
|
|||||||
|
|
||||||
const TestimonialsSchema = new Schema(
|
const TestimonialsSchema = new Schema(
|
||||||
{
|
{
|
||||||
heading: { type: String, default: "", maxlength: 64 },
|
// Toggle visibility on frontend
|
||||||
subheading: { type: String, default: "", maxlength: 40 },
|
enabled: { type: Boolean, default: true },
|
||||||
videoUrl: { type: String, default: "", maxlength: 255 },
|
|
||||||
videoThumbnail: { type: String, default: "", maxlength: 255 },
|
heading: { type: String, default: "" },
|
||||||
|
subheading: { type: String, default: "" },
|
||||||
|
videoUrl: { type: String, default: "" },
|
||||||
|
videoThumbnail: { type: String, default: "" },
|
||||||
items: { type: [TestimonialSchema], default: [] },
|
items: { type: [TestimonialSchema], default: [] },
|
||||||
},
|
},
|
||||||
{ _id: false },
|
{ _id: false },
|
||||||
@@ -180,23 +195,12 @@ const TestimonialsSchema = new Schema(
|
|||||||
|
|
||||||
const VideoGallerySchema = new Schema(
|
const VideoGallerySchema = new Schema(
|
||||||
{
|
{
|
||||||
heading: {
|
// Toggle visibility on frontend
|
||||||
type: String,
|
enabled: { type: Boolean, default: true },
|
||||||
default: "",
|
|
||||||
maxlength: 32,
|
heading: { type: String, default: "" },
|
||||||
validate: {
|
videoUrl: { type: String, default: "" },
|
||||||
validator(value) {
|
thumbnail: { type: String, default: "" },
|
||||||
const words = String(value || "")
|
|
||||||
.trim()
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter(Boolean);
|
|
||||||
return words.length <= 4;
|
|
||||||
},
|
|
||||||
message: "Video gallery heading must be 4 words or fewer.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
videoUrl: { type: String, default: "", maxlength: 255 },
|
|
||||||
thumbnail: { type: String, default: "", maxlength: 255 },
|
|
||||||
},
|
},
|
||||||
{ _id: false },
|
{ _id: false },
|
||||||
);
|
);
|
||||||
@@ -211,9 +215,12 @@ const FaqItemSchema = new Schema(
|
|||||||
|
|
||||||
const FaqSchema = new Schema(
|
const FaqSchema = new Schema(
|
||||||
{
|
{
|
||||||
heading: { type: String, default: "", maxlength: 64 },
|
// Toggle visibility on frontend
|
||||||
subheading: { type: String, default: "", maxlength: 40 },
|
enabled: { type: Boolean, default: true },
|
||||||
description: { type: String, default: "", maxlength: 220 },
|
|
||||||
|
heading: { type: String, default: "" },
|
||||||
|
subheading: { type: String, default: "" },
|
||||||
|
description: { type: String, default: "" },
|
||||||
ctaButton: { type: LinkSchema, default: () => ({}) },
|
ctaButton: { type: LinkSchema, default: () => ({}) },
|
||||||
items: { type: [FaqItemSchema], default: [] },
|
items: { type: [FaqItemSchema], default: [] },
|
||||||
},
|
},
|
||||||
@@ -232,8 +239,11 @@ const AchievementItemSchema = new Schema(
|
|||||||
|
|
||||||
const AchievementsSchema = new Schema(
|
const AchievementsSchema = new Schema(
|
||||||
{
|
{
|
||||||
heading: { type: String, default: "", maxlength: 56 },
|
// Toggle visibility on frontend
|
||||||
subheading: { type: String, default: "", maxlength: 32 },
|
enabled: { type: Boolean, default: true },
|
||||||
|
|
||||||
|
heading: { type: String, default: "" },
|
||||||
|
subheading: { type: String, default: "" },
|
||||||
items: { type: [AchievementItemSchema], default: [] },
|
items: { type: [AchievementItemSchema], default: [] },
|
||||||
},
|
},
|
||||||
{ _id: false },
|
{ _id: false },
|
||||||
@@ -271,6 +281,9 @@ const BrandsSchema = new Schema(
|
|||||||
|
|
||||||
const PartnersSchema = new Schema(
|
const PartnersSchema = new Schema(
|
||||||
{
|
{
|
||||||
|
// Toggle visibility on frontend
|
||||||
|
enabled: { type: Boolean, default: true },
|
||||||
|
|
||||||
visaConsultancy: { type: VisaConsultancySchema, default: () => ({}) },
|
visaConsultancy: { type: VisaConsultancySchema, default: () => ({}) },
|
||||||
brands: { type: BrandsSchema, default: () => ({}) },
|
brands: { type: BrandsSchema, default: () => ({}) },
|
||||||
},
|
},
|
||||||
@@ -296,8 +309,11 @@ const BlogPreviewItemSchema = new Schema(
|
|||||||
|
|
||||||
const BlogPreviewSchema = new Schema(
|
const BlogPreviewSchema = new Schema(
|
||||||
{
|
{
|
||||||
heading: { type: String, default: "", maxlength: 64 },
|
// Toggle visibility on frontend
|
||||||
subheading: { type: String, default: "", maxlength: 40 },
|
enabled: { type: Boolean, default: true },
|
||||||
|
|
||||||
|
heading: { type: String, default: "" },
|
||||||
|
subheading: { type: String, default: "" },
|
||||||
ctaButton: { type: LinkSchema, default: () => ({}) },
|
ctaButton: { type: LinkSchema, default: () => ({}) },
|
||||||
items: { type: [BlogPreviewItemSchema], default: [] },
|
items: { type: [BlogPreviewItemSchema], default: [] },
|
||||||
selectedBlogIds: [{ type: Schema.Types.ObjectId, ref: 'Blog' }],
|
selectedBlogIds: [{ type: Schema.Types.ObjectId, ref: 'Blog' }],
|
||||||
@@ -333,5 +349,8 @@ const HomeSchema = new Schema(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Đảm bảo chỉ có 1 document duy nhất (singleton pattern)
|
||||||
|
HomeSchema.index({ createdAt: 1 }, { unique: false });
|
||||||
|
|
||||||
module.exports = mongoose.model("Home", HomeSchema);
|
module.exports = mongoose.model("Home", HomeSchema);
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
"migrate-fresh": "node scripts/migrate-fresh.js",
|
"migrate-fresh": "node scripts/migrate-fresh.js",
|
||||||
"migrate-status": "node scripts/migrate-status.js",
|
"migrate-status": "node scripts/migrate-status.js",
|
||||||
"migrate-rollback": "node scripts/migrate-rollback.js",
|
"migrate-rollback": "node scripts/migrate-rollback.js",
|
||||||
"make-migration": "node scripts/make-migration.js"
|
"make-migration": "node scripts/make-migration.js",
|
||||||
|
"db:seed": "node scripts/seedDatabase.js"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"cms",
|
"cms",
|
||||||
|
|||||||
22
scripts/cleanup-home-docs.js
Normal file
22
scripts/cleanup-home-docs.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const Home = require('../models/home');
|
||||||
|
|
||||||
|
mongoose.connect(process.env.MONGODB_URI).then(async () => {
|
||||||
|
const docs = await Home.find().sort({ updatedAt: -1 }).lean();
|
||||||
|
console.log('Total docs:', docs.length);
|
||||||
|
|
||||||
|
if (docs.length <= 1) {
|
||||||
|
console.log('Nothing to clean up.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keep = docs[0];
|
||||||
|
const idsToDelete = docs.slice(1).map(d => d._id);
|
||||||
|
await Home.deleteMany({ _id: { $in: idsToDelete } });
|
||||||
|
|
||||||
|
console.log('Kept doc:', keep._id, '| hero.enabled:', keep.hero?.enabled);
|
||||||
|
console.log('Deleted', idsToDelete.length, 'duplicate docs');
|
||||||
|
|
||||||
|
await mongoose.disconnect();
|
||||||
|
});
|
||||||
34
scripts/sync-service-menu-now.js
Normal file
34
scripts/sync-service-menu-now.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* One-time script: sync service menu items from DB into HeaderMenu.
|
||||||
|
* Run: node scripts/sync-service-menu-now.js
|
||||||
|
*/
|
||||||
|
const mongoose = require("mongoose");
|
||||||
|
const dotenv = require("dotenv");
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const Service = require("../models/service");
|
||||||
|
const syncServiceMenu = require("../services/syncServiceMenu");
|
||||||
|
|
||||||
|
const MONGODB_URI = process.env.MONGODB_URI || "mongodb://localhost:27017/hailearning";
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
await mongoose.connect(MONGODB_URI);
|
||||||
|
console.log("✅ Connected to MongoDB");
|
||||||
|
|
||||||
|
const serviceDoc = await Service.findOne().lean();
|
||||||
|
if (!serviceDoc?.services?.items?.length) {
|
||||||
|
console.log("⚠️ No services found in DB.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${serviceDoc.services.items.length} services. Syncing menu...`);
|
||||||
|
await syncServiceMenu(serviceDoc.services.items);
|
||||||
|
|
||||||
|
console.log("✅ Done.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error("❌ Error:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
57
services/syncServiceMenu.js
Normal file
57
services/syncServiceMenu.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Sync HeaderMenu children of the "Services" menu item
|
||||||
|
* to match the current list of services in the database.
|
||||||
|
*
|
||||||
|
* Strategy:
|
||||||
|
* - Find the HeaderMenu item whose url === '/services'
|
||||||
|
* - Delete all its direct children
|
||||||
|
* - Re-create one child per service item (url = /services/<slug>)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const HeaderMenu = require("../models/headerMenu");
|
||||||
|
const slugify = require("slugify");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array} serviceItems - array of service objects { slug, name }
|
||||||
|
*/
|
||||||
|
const syncServiceMenu = async (serviceItems = []) => {
|
||||||
|
try {
|
||||||
|
// 1. Find the "Services" parent menu item
|
||||||
|
const servicesParent = await HeaderMenu.findOne({ url: "/services" });
|
||||||
|
|
||||||
|
if (!servicesParent) {
|
||||||
|
console.warn("[syncServiceMenu] No HeaderMenu item with url=/services found. Skipping sync.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentId = servicesParent._id;
|
||||||
|
|
||||||
|
// 2. Remove all existing children of that parent
|
||||||
|
await HeaderMenu.deleteMany({ parentId });
|
||||||
|
|
||||||
|
// 3. Re-create one child per service
|
||||||
|
const ops = serviceItems
|
||||||
|
.filter((s) => s && s.slug && s.name)
|
||||||
|
.map((s, index) => ({
|
||||||
|
title: s.name,
|
||||||
|
slug: slugify(s.name, { lower: true, strict: true }),
|
||||||
|
url: `/services/${s.slug}`,
|
||||||
|
parentId,
|
||||||
|
order: index + 1,
|
||||||
|
status: "active",
|
||||||
|
type: "internal",
|
||||||
|
is_maintainance: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (ops.length > 0) {
|
||||||
|
await HeaderMenu.insertMany(ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[syncServiceMenu] Synced ${ops.length} service menu items under parentId=${parentId}`);
|
||||||
|
} catch (err) {
|
||||||
|
// Non-fatal – log but don't crash the main request
|
||||||
|
console.error("[syncServiceMenu] Error syncing service menu:", err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = syncServiceMenu;
|
||||||
@@ -41,14 +41,9 @@
|
|||||||
aria-selected="false"><i class="fas fa-info-circle me-2"></i>Intro</a>
|
aria-selected="false"><i class="fas fa-info-circle me-2"></i>Intro</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link <%= locals.activeTab === 'mission' ? 'active' : '' %>" id="mission-tab"
|
<a class="nav-link <%= locals.activeTab === 'mission-vision' ? 'active' : '' %>" id="mission-vision-tab"
|
||||||
data-bs-toggle="tab" href="#mission" role="tab"
|
data-bs-toggle="tab" href="#mission-vision" role="tab"
|
||||||
aria-selected="false"><i class="fas fa-bullseye me-2"></i>Mission</a>
|
aria-selected="false"><i class="fas fa-bullseye me-2"></i>Mission & Vision</a>
|
||||||
</li>
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<a class="nav-link <%= locals.activeTab === 'features' ? 'active' : '' %>" id="features-tab"
|
|
||||||
data-bs-toggle="tab" href="#features" role="tab"
|
|
||||||
aria-selected="false"><i class="fas fa-star me-2"></i>Features</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link <%= locals.activeTab === 'news' ? 'active' : '' %>" id="news-tab"
|
<a class="nav-link <%= locals.activeTab === 'news' ? 'active' : '' %>" id="news-tab"
|
||||||
@@ -148,7 +143,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mission Tab -->
|
<!-- Mission Tab -->
|
||||||
<div class="tab-pane fade <%= locals.activeTab === 'mission' ? 'show active' : '' %>" id="mission" role="tabpanel">
|
<div class="tab-pane fade <%= locals.activeTab === 'mission-vision' ? 'show active' : '' %>" id="mission-vision" role="tabpanel">
|
||||||
<div class="card border shadow-sm">
|
<div class="card border shadow-sm">
|
||||||
<div class="card-header bg-white">
|
<div class="card-header bg-white">
|
||||||
<h6 class="mb-0"><i class="fas fa-bullseye me-2"></i>Mission Section</h6>
|
<h6 class="mb-0"><i class="fas fa-bullseye me-2"></i>Mission Section</h6>
|
||||||
@@ -223,10 +218,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Features Tab -->
|
|
||||||
<div class="tab-pane fade <%= locals.activeTab === 'features' ? 'show active' : '' %>" id="features" role="tabpanel">
|
|
||||||
<div class="card border shadow-sm">
|
<div class="card border shadow-sm">
|
||||||
<div class="card-header bg-white">
|
<div class="card-header bg-white">
|
||||||
<h6 class="mb-0"><i class="fas fa-star me-2"></i>Features Section</h6>
|
<h6 class="mb-0"><i class="fas fa-star me-2"></i>Features Section</h6>
|
||||||
@@ -372,11 +364,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script id="serverAboutData" type="application/json">
|
||||||
|
<%- JSON.stringify(locals.data || {}) %>
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let originalFormData = null;
|
let originalFormData = null;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
originalFormData = <%- JSON.stringify(data) %>;
|
// Lấy nội dung text từ thẻ script ẩn và parse nó thành Object
|
||||||
|
const dataElement = document.getElementById('serverAboutData');
|
||||||
|
|
||||||
|
try {
|
||||||
|
originalFormData = JSON.parse(dataElement.textContent || '{}');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Lỗi khi parse dữ liệu từ server:', error);
|
||||||
|
originalFormData = {}; // Giá trị mặc định an toàn nếu parse xịt
|
||||||
|
}
|
||||||
|
|
||||||
updateAllJsonInputs(originalFormData);
|
updateAllJsonInputs(originalFormData);
|
||||||
initializeFormHandlers();
|
initializeFormHandlers();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
<!-- Achievements Tab -->
|
<!-- Achievements Tab -->
|
||||||
<div class="tab-pane fade" id="achievements" role="tabpanel">
|
<div class="tab-pane fade" id="achievements" role="tabpanel">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- Basic Info -->
|
<!-- Basic Info -->
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
<div class="card border shadow-sm mb-1">
|
||||||
|
<div class="card-header bg-white d-flex justify-content-center align-items-center">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="achievementEnabled"
|
||||||
|
<%=(data.achievements?.enabled !==false ) ? 'checked' : '' %>>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card border shadow-sm">
|
<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">
|
<h6 class="mb-0">
|
||||||
<i class="fas fa-chart-pie me-2"></i>General Information
|
<i class="fas fa-chart-pie me-2"></i>General Information
|
||||||
</h6>
|
</h6>
|
||||||
@@ -13,27 +22,13 @@
|
|||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label fw-medium">Heading</label>
|
<label class="form-label fw-medium">Heading</label>
|
||||||
<input
|
<input type="text" class="form-control" id="achievementsHeading"
|
||||||
type="text"
|
value="<%= data.achievements?.heading || '' %>" placeholder="e.g., Our Achievements in Numbers" />
|
||||||
class="form-control"
|
|
||||||
id="achievementsHeading"
|
|
||||||
value="<%= data.achievements?.heading || '' %>"
|
|
||||||
placeholder="e.g., Our Achievements in Numbers"
|
|
||||||
maxlength="56"
|
|
||||||
data-maxlength="56"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label fw-medium">Subheading</label>
|
<label class="form-label fw-medium">Subheading</label>
|
||||||
<input
|
<input type="text" class="form-control" id="achievementsSubheading"
|
||||||
type="text"
|
value="<%= data.achievements?.subheading || '' %>" placeholder="e.g., Did You Know" />
|
||||||
class="form-control"
|
|
||||||
id="achievementsSubheading"
|
|
||||||
value="<%= data.achievements?.subheading || '' %>"
|
|
||||||
placeholder="e.g., Did You Know"
|
|
||||||
maxlength="32"
|
|
||||||
data-maxlength="32"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,61 +44,33 @@
|
|||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body" id="achievementItemsContainer">
|
<div class="card-body" id="achievementItemsContainer">
|
||||||
<% for(let i=0; i<4; i++) {
|
<% for(let i=0; i<4; i++) { const item=(data.achievements?.items && data.achievements.items[i]) || {}; %>
|
||||||
const item = (data.achievements?.items && data.achievements.items[i]) || {};
|
|
||||||
%>
|
|
||||||
<div class="card mb-3 bg-light border achievement-item">
|
<div class="card mb-3 bg-light border achievement-item">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h6 class="card-title fw-bold mb-0">Achievement #<%= i + 1 %></h6>
|
<h6 class="card-title fw-bold mb-0">Achievement #<%= i + 1 %>
|
||||||
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label fw-medium">Value (Number)</label>
|
<label class="form-label fw-medium">Value (Number)</label>
|
||||||
<input
|
<input type="text" class="form-control achievement-value" value="<%= item.value || '' %>"
|
||||||
type="text"
|
placeholder="e.g., 95" />
|
||||||
class="form-control achievement-value"
|
|
||||||
id="achievementValue_<%= i %>"
|
|
||||||
value="<%= item.value || '' %>"
|
|
||||||
placeholder="e.g., 95"
|
|
||||||
maxlength="6"
|
|
||||||
data-maxlength="6"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label fw-medium">Suffix</label>
|
<label class="form-label fw-medium">Suffix</label>
|
||||||
<input
|
<input type="text" class="form-control achievement-suffix" value="<%= item.suffix || '' %>"
|
||||||
type="text"
|
placeholder="e.g., %" />
|
||||||
class="form-control achievement-suffix"
|
|
||||||
id="achievementSuffix_<%= i %>"
|
|
||||||
value="<%= item.suffix || '' %>"
|
|
||||||
placeholder="e.g., %"
|
|
||||||
maxlength="4"
|
|
||||||
data-maxlength="4"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label fw-medium">Label</label>
|
<label class="form-label fw-medium">Label</label>
|
||||||
<input
|
<input type="text" class="form-control achievement-label" value="<%= item.label || '' %>"
|
||||||
type="text"
|
placeholder="e.g., Visa Success Rate" />
|
||||||
class="form-control achievement-label"
|
|
||||||
id="achievementLabel_<%= i %>"
|
|
||||||
value="<%= item.label || '' %>"
|
|
||||||
placeholder="e.g., Visa Success Rate"
|
|
||||||
maxlength="40"
|
|
||||||
data-maxlength="40"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<label class="form-label fw-medium">Description</label>
|
<label class="form-label fw-medium">Description</label>
|
||||||
<textarea
|
<textarea class="form-control achievement-description" rows="2"
|
||||||
class="form-control achievement-description"
|
placeholder="Short description of this achievement"><%= item.description || '' %></textarea>
|
||||||
id="achievementDescription_<%= i %>"
|
|
||||||
rows="2"
|
|
||||||
placeholder="Short description of this achievement"
|
|
||||||
maxlength="120"
|
|
||||||
data-maxlength="120"
|
|
||||||
><%= item.description || '' %></textarea>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,8 +85,11 @@
|
|||||||
<script>
|
<script>
|
||||||
// Đăng ký scraper cho phần achievements
|
// Đăng ký scraper cho phần achievements
|
||||||
window.homeScrapers = window.homeScrapers || {};
|
window.homeScrapers = window.homeScrapers || {};
|
||||||
window.homeScrapers.achievements = function() {
|
window.homeScrapers.achievements = function () {
|
||||||
const items = [];
|
const items = [];
|
||||||
|
|
||||||
|
const enabled = document.getElementById("achievementEnabled")?.checked === true;
|
||||||
|
|
||||||
document.querySelectorAll('.achievement-item').forEach(el => {
|
document.querySelectorAll('.achievement-item').forEach(el => {
|
||||||
items.push({
|
items.push({
|
||||||
value: el.querySelector('.achievement-value').value,
|
value: el.querySelector('.achievement-value').value,
|
||||||
@@ -132,7 +102,8 @@
|
|||||||
return {
|
return {
|
||||||
heading: document.getElementById('achievementsHeading').value,
|
heading: document.getElementById('achievementsHeading').value,
|
||||||
subheading: document.getElementById('achievementsSubheading').value,
|
subheading: document.getElementById('achievementsSubheading').value,
|
||||||
items: items
|
items: items,
|
||||||
|
enabled
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
<!-- Blog Preview Tab -->
|
<!-- Blog Preview Tab -->
|
||||||
<div class="tab-pane fade" id="blogpreview" role="tabpanel">
|
<div class="tab-pane fade" id="blogpreview" role="tabpanel">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- Basic Info -->
|
<!-- Basic Info -->
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
<div class="card border shadow-sm mb-1">
|
||||||
|
<div class="card-header bg-white d-flex justify-content-center align-items-center">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="blogpreviewEnabled"
|
||||||
|
<%=(data.blogPreview?.enabled !==false ) ? 'checked' : '' %>>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card border shadow-sm">
|
<div class="card border shadow-sm">
|
||||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||||
<h6 class="mb-0">
|
<h6 class="mb-0">
|
||||||
@@ -148,6 +157,9 @@
|
|||||||
window.homeScrapers = window.homeScrapers || {};
|
window.homeScrapers = window.homeScrapers || {};
|
||||||
window.homeScrapers.blogPreview = () => {
|
window.homeScrapers.blogPreview = () => {
|
||||||
const selectedIds = [];
|
const selectedIds = [];
|
||||||
|
|
||||||
|
const enabled = document.getElementById("blogpreviewEnabled")?.checked === true;
|
||||||
|
|
||||||
document.querySelectorAll('.blog-checkbox:checked').forEach(cb => {
|
document.querySelectorAll('.blog-checkbox:checked').forEach(cb => {
|
||||||
selectedIds.push(cb.value);
|
selectedIds.push(cb.value);
|
||||||
});
|
});
|
||||||
@@ -162,7 +174,8 @@
|
|||||||
href: document.getElementById('blogPreviewCtaHref').value
|
href: document.getElementById('blogPreviewCtaHref').value
|
||||||
},
|
},
|
||||||
selectedBlogIds: selectedIds,
|
selectedBlogIds: selectedIds,
|
||||||
items: [] // Server side will handle full items content
|
items: [],// Server side will handle full items content
|
||||||
|
enabled
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
<!-- FAQ Tab -->
|
<!-- FAQ Tab -->
|
||||||
<div class="tab-pane fade" id="faq" role="tabpanel">
|
<div class="tab-pane fade" id="faq" role="tabpanel">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- Basic Info -->
|
<!-- Basic Info -->
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
<div class="card border shadow-sm mb-1">
|
||||||
|
<div class="card-header bg-white d-flex justify-content-center align-items-center">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="faqEnabled"
|
||||||
|
<%= (data.faq?.enabled !== false ) ? 'checked' : '' %>>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card border shadow-sm">
|
<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">
|
<h6 class="mb-0">
|
||||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||||
</h6>
|
</h6>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="faqEnabled"
|
||||||
|
<%= (data.faq?.enabled !== false ) ? 'checked' : '' %>>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
@@ -126,6 +139,8 @@
|
|||||||
window.homeScrapers.faq = () => {
|
window.homeScrapers.faq = () => {
|
||||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||||
|
|
||||||
|
const enabled = document.getElementById('faqEnabled')?.checked === true;
|
||||||
|
|
||||||
const items = [];
|
const items = [];
|
||||||
document.querySelectorAll(".faq-item").forEach((el, idx) => {
|
document.querySelectorAll(".faq-item").forEach((el, idx) => {
|
||||||
const index = el.getAttribute("data-index") || idx;
|
const index = el.getAttribute("data-index") || idx;
|
||||||
@@ -146,7 +161,8 @@
|
|||||||
label: getVal("faqCtaLabel"),
|
label: getVal("faqCtaLabel"),
|
||||||
href: getVal("faqCtaHref")
|
href: getVal("faqCtaHref")
|
||||||
},
|
},
|
||||||
items
|
items,
|
||||||
|
enabled
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
<div class="tab-pane fade" id="floatingcontact" role="tabpanel">
|
<div class="tab-pane fade" id="floatingcontact" role="tabpanel">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
<div class="card border shadow-sm mb-1">
|
||||||
|
<div class="card-header bg-white d-flex justify-content-center align-items-center">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="floatingContactEnabled"
|
||||||
|
<%=(data.floatingContact?.enabled !== false ) ? 'checked' : '' %>>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card border shadow-sm">
|
<div class="card border shadow-sm">
|
||||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||||
<h6 class="mb-0">
|
<h6 class="mb-0">
|
||||||
@@ -11,21 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-4 align-items-start">
|
<div class="row g-4 align-items-start">
|
||||||
<div class="col-12">
|
|
||||||
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 p-3 border rounded-3 bg-light-subtle">
|
|
||||||
<div>
|
|
||||||
<div class="fw-semibold">Widget visibility</div>
|
|
||||||
<small class="text-muted">Enable or disable the floating contact widget on the homepage.</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-check form-switch m-0">
|
|
||||||
<input class="form-check-input" type="checkbox" id="floatingContactEnabled"
|
|
||||||
<%= data.floatingContact?.enabled !== false ? 'checked' : '' %> />
|
|
||||||
<label class="form-check-label fw-medium" for="floatingContactEnabled">
|
|
||||||
Enable floating widget
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
<label class="form-label fw-medium">Panel Title</label>
|
<label class="form-label fw-medium">Panel Title</label>
|
||||||
<input type="text" class="form-control" id="floatingContactPanelTitle"
|
<input type="text" class="form-control" id="floatingContactPanelTitle"
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
<!-- Hero Tab -->
|
<!-- Hero Tab -->
|
||||||
<div class="tab-pane fade show active" id="hero" role="tabpanel">
|
<div class="tab-pane fade show active" id="hero" role="tabpanel">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
<div class="card border shadow-sm mb-1">
|
||||||
|
<div class="card-header bg-white d-flex justify-content-center align-items-center">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="heroEnabled"
|
||||||
|
<%=(data.hero?.enabled !== false ) ? 'checked' : '' %>>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card border shadow-sm mb-3">
|
<div class="card border shadow-sm mb-3">
|
||||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||||
<h6 class="mb-0">
|
<h6 class="mb-0">
|
||||||
@@ -194,6 +203,7 @@
|
|||||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||||
|
|
||||||
const backgroundImage = getVal("heroBackgroundImage");
|
const backgroundImage = getVal("heroBackgroundImage");
|
||||||
|
const enabled = document.getElementById("heroEnabled")?.checked === true;
|
||||||
|
|
||||||
const slides = [];
|
const slides = [];
|
||||||
const slideEls = document.querySelectorAll(".hero-slide-item");
|
const slideEls = document.querySelectorAll(".hero-slide-item");
|
||||||
@@ -229,6 +239,7 @@
|
|||||||
const first = slides[0] || {};
|
const first = slides[0] || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
enabled,
|
||||||
backgroundImage,
|
backgroundImage,
|
||||||
slides,
|
slides,
|
||||||
title: first.title || "",
|
title: first.title || "",
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
<!-- Partners Tab -->
|
<!-- Partners Tab -->
|
||||||
<div class="tab-pane fade" id="partners" role="tabpanel">
|
<div class="tab-pane fade" id="partners" role="tabpanel">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- Visa Consultancy Awards -->
|
<!-- Visa Consultancy Awards -->
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
<div class="card border shadow-sm mb-1">
|
||||||
|
<div class="card-header bg-white d-flex justify-content-center align-items-center">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="partnersEnabled"
|
||||||
|
<%=(data.partners?.enabled !==false ) ? 'checked' : '' %>>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card border shadow-sm">
|
<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">
|
<h6 class="mb-0">
|
||||||
<i class="fas fa-award me-2"></i>Awards & Certifications (Fixed 4 Items)
|
<i class="fas fa-award me-2"></i>Awards & Certifications (Fixed 4 Items)
|
||||||
</h6>
|
</h6>
|
||||||
@@ -12,35 +21,39 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<small class="text-muted d-block mb-3">Award icons on the homepage display at roughly 124x124px. Recommended uploads are square images or transparent logos at least 248x248px.</small>
|
<small class="text-muted d-block mb-3">Award icons on the homepage display at roughly 124x124px. Recommended uploads are square images or transparent logos at least 248x248px.</small>
|
||||||
<div id="visaConsultancyContainer">
|
<div id="visaConsultancyContainer">
|
||||||
<% for(let i=0; i<4; i++) {
|
<% for(let i=0; i<4; i++) { const item=(data.partners?.visaConsultancy?.items &&
|
||||||
const item = (data.partners?.visaConsultancy?.items && data.partners.visaConsultancy.items[i]) || {};
|
data.partners.visaConsultancy.items[i]) || {}; %>
|
||||||
%>
|
|
||||||
<div class="card mb-3 bg-light border visa-item">
|
<div class="card mb-3 bg-light border visa-item">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h6 class="card-title fw-bold mb-0">Award #<%= i + 1 %></h6>
|
<h6 class="card-title fw-bold mb-0">Award #<%= i + 1 %>
|
||||||
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<label class="form-label fw-medium">Award Name</label>
|
<label class="form-label fw-medium">Award Name</label>
|
||||||
<input type="text" class="form-control visa-name" id="visaName_<%= i %>" value="<%= item.name || '' %>" placeholder="Award Name" maxlength="48" data-maxlength="48" />
|
<input type="text" class="form-control visa-name" value="<%= item.name || '' %>"
|
||||||
|
placeholder="Award Name" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label fw-medium">Year</label>
|
<label class="form-label fw-medium">Year</label>
|
||||||
<input type="text" class="form-control visa-year" id="visaYear_<%= i %>" value="<%= item.year || '' %>" placeholder="e.g., 2025" maxlength="8" data-maxlength="8" />
|
<input type="text" class="form-control visa-year" value="<%= item.year || '' %>"
|
||||||
|
placeholder="e.g., 2025" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<label class="form-label fw-medium">Icon / Logo</label>
|
<label class="form-label fw-medium">Icon / Logo</label>
|
||||||
<small class="text-muted d-block mb-1">Recommended: a 248x248px square image or transparent logo for crisp rendering.</small>
|
<small class="text-muted d-block mb-1">Recommended: a 248x248px square image or transparent logo for crisp rendering.</small>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control visa-icon" id="visaIcon_<%= i %>" value="<%= item.icon || '' %>" maxlength="255" data-maxlength="255" />
|
<input type="text" class="form-control visa-icon" id="visaIcon_<%= i %>"
|
||||||
|
value="<%= item.icon || '' %>" />
|
||||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||||
data-target-input="visaIcon_<%= i %>" data-image-type="home">
|
data-target-input="visaIcon_<%= i %>" data-image-type="home">
|
||||||
<i class="fas fa-upload me-1"></i>Upload
|
<i class="fas fa-upload me-1"></i>Upload
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 preview-container">
|
<div class="mt-2 preview-container">
|
||||||
<img src="<%= item.icon || '' %>" class="img-thumbnail <%= item.icon ? '' : 'd-none' %>" style="height: 60px; object-fit: contain;">
|
<img src="<%= item.icon || '' %>" class="img-thumbnail <%= item.icon ? '' : 'd-none' %>"
|
||||||
|
style="height: 60px; object-fit: contain;">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,12 +85,14 @@
|
|||||||
<div class="card-body py-2">
|
<div class="card-body py-2">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<span class="small fw-bold">Brand Logo</span>
|
<span class="small fw-bold">Brand Logo</span>
|
||||||
<button type="button" class="btn btn-link text-danger p-0" onclick="this.closest('.brand-partner-item').remove()">
|
<button type="button" class="btn btn-link text-danger p-0"
|
||||||
|
onclick="this.closest('.brand-partner-item').remove()">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input type="text" class="form-control brand-logo-input" id="brandLogo_<%= index %>" value="<%= item.logo || '' %>" maxlength="255" data-maxlength="255" />
|
<input type="text" class="form-control brand-logo-input" id="brandLogo_<%= index %>"
|
||||||
|
value="<%= item.logo || '' %>" />
|
||||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||||
data-target-input="brandLogo_<%= index %>" data-image-type="home">
|
data-target-input="brandLogo_<%= index %>" data-image-type="home">
|
||||||
<i class="fas fa-upload"></i>
|
<i class="fas fa-upload"></i>
|
||||||
@@ -85,7 +100,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<small class="text-muted d-block mt-2">Recommended: SVG or a 320x96px horizontal image so the logo stays sharp in the slider.</small>
|
<small class="text-muted d-block mt-2">Recommended: SVG or a 320x96px horizontal image so the logo stays sharp in the slider.</small>
|
||||||
<div class="mt-2 text-center preview-container">
|
<div class="mt-2 text-center preview-container">
|
||||||
<img src="<%= item.logo || '' %>" class="img-thumbnail <%= item.logo ? '' : 'd-none' %>" style="height: 50px; object-fit: contain;">
|
<img src="<%= item.logo || '' %>" class="img-thumbnail <%= item.logo ? '' : 'd-none' %>"
|
||||||
|
style="height: 50px; object-fit: contain;">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -101,8 +117,11 @@
|
|||||||
<script>
|
<script>
|
||||||
// Thu thập dữ liệu partners
|
// Thu thập dữ liệu partners
|
||||||
window.homeScrapers = window.homeScrapers || {};
|
window.homeScrapers = window.homeScrapers || {};
|
||||||
window.homeScrapers.partners = function() {
|
window.homeScrapers.partners = function () {
|
||||||
const visaItems = [];
|
const visaItems = [];
|
||||||
|
|
||||||
|
const enabled = document.getElementById('partnersEnabled')?.checked === true;
|
||||||
|
|
||||||
document.querySelectorAll('.visa-item').forEach(el => {
|
document.querySelectorAll('.visa-item').forEach(el => {
|
||||||
visaItems.push({
|
visaItems.push({
|
||||||
name: el.querySelector('.visa-name').value,
|
name: el.querySelector('.visa-name').value,
|
||||||
@@ -119,7 +138,8 @@
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
visaConsultancy: { items: visaItems },
|
visaConsultancy: { items: visaItems },
|
||||||
brands: { items: brandItems }
|
brands: { items: brandItems },
|
||||||
|
enabled
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
<!-- Testimonials Tab -->
|
<!-- Testimonials Tab -->
|
||||||
<div class="tab-pane fade" id="testimonials" role="tabpanel">
|
<div class="tab-pane fade" id="testimonials" role="tabpanel">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- Basic Info -->
|
<!-- Basic Info -->
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
<div class="card border shadow-sm mb-1">
|
||||||
|
<div class="card-header bg-white d-flex justify-content-center align-items-center">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="testimonialEnabled"
|
||||||
|
<%=(data.testimonials?.enabled !==false ) ? 'checked' : '' %>>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card border shadow-sm">
|
<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">
|
<h6 class="mb-0">
|
||||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||||
</h6>
|
</h6>
|
||||||
@@ -126,6 +135,8 @@
|
|||||||
window.homeScrapers.testimonials = () => {
|
window.homeScrapers.testimonials = () => {
|
||||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||||
|
|
||||||
|
const enabled = document.getElementById("testimonialEnabled")?.checked === true;
|
||||||
|
|
||||||
const items = [];
|
const items = [];
|
||||||
document.querySelectorAll(".testimonial-item").forEach((el, idx) => {
|
document.querySelectorAll(".testimonial-item").forEach((el, idx) => {
|
||||||
const index = el.getAttribute("data-index") || idx;
|
const index = el.getAttribute("data-index") || idx;
|
||||||
@@ -156,6 +167,7 @@
|
|||||||
videoUrl: getVal("testimonialsVideoUrl"),
|
videoUrl: getVal("testimonialsVideoUrl"),
|
||||||
videoThumbnail: getVal("testimonialsVideoThumbnail"),
|
videoThumbnail: getVal("testimonialsVideoThumbnail"),
|
||||||
items,
|
items,
|
||||||
|
enabled
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
<!-- Video Gallery Tab -->
|
<!-- Video Gallery Tab -->
|
||||||
<div class="tab-pane fade" id="videogallery" role="tabpanel">
|
<div class="tab-pane fade" id="videogallery" role="tabpanel">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
<div class="card border shadow-sm mb-1">
|
||||||
|
<div class="card-header bg-white d-flex justify-content-center align-items-center">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="videoGalleryEnabled"
|
||||||
|
<%=(data.videoGallery?.enabled !==false ) ? 'checked' : '' %>>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card border shadow-sm">
|
<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">
|
<h6 class="mb-0">
|
||||||
<i class="fas fa-video me-2"></i>Video Gallery
|
<i class="fas fa-video me-2"></i>Video Gallery
|
||||||
</h6>
|
</h6>
|
||||||
@@ -54,10 +64,12 @@
|
|||||||
window.homeScrapers.videoGallery = () => {
|
window.homeScrapers.videoGallery = () => {
|
||||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||||
|
|
||||||
|
const enabled = document.getElementById("videoGalleryEnabled")?.checked === true;
|
||||||
return {
|
return {
|
||||||
heading: getVal("videoGalleryHeading"),
|
heading: getVal("videoGalleryHeading"),
|
||||||
videoUrl: getVal("videoGalleryVideoUrl"),
|
videoUrl: getVal("videoGalleryVideoUrl"),
|
||||||
thumbnail: getVal("videoGalleryThumbnail"),
|
thumbnail: getVal("videoGalleryThumbnail"),
|
||||||
|
enabled
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
<!-- Visa Countries Tab -->
|
<!-- Visa Countries Tab -->
|
||||||
<div class="tab-pane fade" id="visacountries" role="tabpanel">
|
<div class="tab-pane fade" id="visacountries" role="tabpanel">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- Basic Info -->
|
<!-- Basic Info -->
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
<div class="card border shadow-sm mb-1">
|
||||||
|
<div class="card-header bg-white d-flex justify-content-center align-items-center">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="visaCountriesEnabled"
|
||||||
|
<%=(data.visaCountries?.enabled !==false ) ? 'checked' : '' %>>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card border shadow-sm">
|
<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">
|
<h6 class="mb-0">
|
||||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||||
</h6>
|
</h6>
|
||||||
@@ -126,6 +135,8 @@
|
|||||||
window.homeScrapers.visaCountries = () => {
|
window.homeScrapers.visaCountries = () => {
|
||||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||||
|
|
||||||
|
const enabled = document.getElementById("visaCountriesEnabled")?.checked === true
|
||||||
|
|
||||||
const visaTypesRaw = getVal("visaCountriesVisaTypes_0");
|
const visaTypesRaw = getVal("visaCountriesVisaTypes_0");
|
||||||
const visaTypes = visaTypesRaw ? visaTypesRaw.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
const visaTypes = visaTypesRaw ? visaTypesRaw.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
||||||
|
|
||||||
@@ -138,6 +149,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
enabled,
|
||||||
heading: getVal("visaCountriesHeading"),
|
heading: getVal("visaCountriesHeading"),
|
||||||
subheading: getVal("visaCountriesSubheading"),
|
subheading: getVal("visaCountriesSubheading"),
|
||||||
description: getVal("visaCountriesDescription"),
|
description: getVal("visaCountriesDescription"),
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
<!-- Visa Solutions Tab -->
|
<!-- Visa Solutions Tab -->
|
||||||
<div class="tab-pane fade" id="visasolutions" role="tabpanel">
|
<div class="tab-pane fade" id="visasolutions" role="tabpanel">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- Basic Info -->
|
<!-- Basic Info -->
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
<div class="card border shadow-sm mb-1">
|
||||||
|
<div class="card-header bg-white d-flex justify-content-center align-items-center">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="visaSolutionsEnabled"
|
||||||
|
<%= (data.visaSolutions?.enabled !== false ) ? 'checked' : '' %>>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card border shadow-sm">
|
<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">
|
<h6 class="mb-0">
|
||||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||||
</h6>
|
</h6>
|
||||||
@@ -91,6 +100,8 @@
|
|||||||
window.homeScrapers.visaSolutions = () => {
|
window.homeScrapers.visaSolutions = () => {
|
||||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||||
|
|
||||||
|
const enabled = document.getElementById("visaSolutionsEnabled")?.checked === true;
|
||||||
|
|
||||||
const items = [];
|
const items = [];
|
||||||
document.querySelectorAll(".visa-solution-item").forEach((el, idx) => {
|
document.querySelectorAll(".visa-solution-item").forEach((el, idx) => {
|
||||||
const index = el.getAttribute("data-index") || idx;
|
const index = el.getAttribute("data-index") || idx;
|
||||||
@@ -109,6 +120,7 @@
|
|||||||
heading: getVal("visaSolutionsHeading"),
|
heading: getVal("visaSolutionsHeading"),
|
||||||
subheading: getVal("visaSolutionsSubheading"),
|
subheading: getVal("visaSolutionsSubheading"),
|
||||||
items,
|
items,
|
||||||
|
enabled
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
<!-- Why Choose Us Tab -->
|
<!-- Why Choose Us Tab -->
|
||||||
<div class="tab-pane fade" id="whychooseus" role="tabpanel">
|
<div class="tab-pane fade" id="whychooseus" role="tabpanel">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- Basic Info -->
|
<!-- Basic Info -->
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
<div class="card border shadow-sm mb-1">
|
||||||
|
<div class="card-header bg-white d-flex justify-content-center align-items-center">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="whyChooseUsEnabled"
|
||||||
|
<%= (data.whyChooseUs.enabled !== false ) ? 'checked' : '' %>>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card border shadow-sm mb-3">
|
<div class="card border shadow-sm mb-3">
|
||||||
<div class="card-header bg-white">
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||||
<h6 class="mb-0">
|
<h6 class="mb-0">
|
||||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||||
</h6>
|
</h6>
|
||||||
@@ -197,6 +206,7 @@
|
|||||||
window.homeScrapers = window.homeScrapers || {};
|
window.homeScrapers = window.homeScrapers || {};
|
||||||
window.homeScrapers.whyChooseUs = () => {
|
window.homeScrapers.whyChooseUs = () => {
|
||||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||||
|
const enabled = document.getElementById("whyChooseUsEnabled")?.checked === true;
|
||||||
|
|
||||||
// Collect items
|
// Collect items
|
||||||
const items = [];
|
const items = [];
|
||||||
@@ -234,6 +244,7 @@
|
|||||||
secondaryImage: getVal("whyChooseUsSecondaryImage"),
|
secondaryImage: getVal("whyChooseUsSecondaryImage"),
|
||||||
items,
|
items,
|
||||||
features,
|
features,
|
||||||
|
enabled,
|
||||||
ctaButton: {
|
ctaButton: {
|
||||||
label: getVal("whyChooseUsCtaLabel"),
|
label: getVal("whyChooseUsCtaLabel"),
|
||||||
href: getVal("whyChooseUsCtaHref"),
|
href: getVal("whyChooseUsCtaHref"),
|
||||||
|
|||||||
Reference in New Issue
Block a user