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:
@@ -1,138 +1,56 @@
|
|||||||
const { addBaseUrlToImages } = require("../utils/imageHelper");
|
const { addBaseUrlToImages, getFullImageUrl } = require("../utils/imageHelper");
|
||||||
const Home = require("../models/home");
|
const Home = require("../models/home");
|
||||||
|
const Blog = require("../models/blog");
|
||||||
|
|
||||||
// -------------------- Helpers --------------------
|
// Các hàm hỗ trợ
|
||||||
|
const getHomeDoc = async () => Home.findOne().sort({ updatedAt: -1 });
|
||||||
|
const getHomeData = async () => (await getHomeDoc())?.toObject() || {};
|
||||||
|
|
||||||
const getHomeDoc = async () => {
|
|
||||||
// Keep newest document as the source of truth
|
|
||||||
return await Home.findOne().sort({ updatedAt: -1 });
|
|
||||||
};
|
|
||||||
|
|
||||||
const getHomeData = async () => {
|
|
||||||
const doc = await Home.findOne().sort({ updatedAt: -1 }).lean();
|
|
||||||
return doc || {};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default structure used by the current CMS Home admin UI (`views/admin/home/index.ejs`).
|
|
||||||
* This is intentionally permissive; the Home model itself also supports the Next.js
|
|
||||||
* structure from `hailearning.edu.vn/app/home.json`.
|
|
||||||
*/
|
|
||||||
const getDefaultHomeData = () => ({
|
const getDefaultHomeData = () => ({
|
||||||
hero: {
|
hero: { title: "", subtitle: "", description: "", backgroundImage: "", videoUrl: "", primaryButton: {}, secondaryButton: {} },
|
||||||
title: "",
|
whyChooseUs: { heading: "", subheading: "", description: "", items: [], features: [], ctaButton: {} },
|
||||||
description: "",
|
visaSolutions: { heading: "", subheading: "", items: [] },
|
||||||
backgroundImage: "",
|
visaCountries: { heading: "", subheading: "", description: "", countries: [], ctaButton: {} },
|
||||||
button: { label: "Book Your Adventure", href: "/booking" },
|
testimonials: { heading: "", subheading: "", videoUrl: "", videoThumbnail: "", items: [] },
|
||||||
contactBox: {
|
videoGallery: { heading: "", videoUrl: "", thumbnail: "" },
|
||||||
welcomeText: "",
|
faq: { heading: "", subheading: "", description: "", ctaButton: {}, items: [] },
|
||||||
phone: { label: "Call us", number: "", href: "" },
|
achievements: { heading: "", subheading: "", items: [] },
|
||||||
email: { label: "Email", address: "", href: "" },
|
partners: { visaConsultancy: { items: [] }, brands: { items: [] } },
|
||||||
workingHours: { label: "Working Hours", hours: "" },
|
blogPreview: {
|
||||||
},
|
heading: "Latest Insights & Updates",
|
||||||
},
|
subheading: "Visa Tips & Guides",
|
||||||
about: {
|
ctaButton: { label: "View All Articles", href: "/blog" },
|
||||||
title: "",
|
|
||||||
subtitle: "",
|
|
||||||
description: "",
|
|
||||||
images: { mainImage1: "", mainImage2: "", avatars: [] },
|
|
||||||
features: [],
|
|
||||||
quote: "",
|
|
||||||
button: { label: "", href: "" },
|
|
||||||
stats: { customerCount: 0, customerLabel: "" },
|
|
||||||
},
|
|
||||||
missionVision: {
|
|
||||||
title: "",
|
|
||||||
subtitle: "",
|
|
||||||
backgroundImage: "",
|
|
||||||
cards: [],
|
|
||||||
},
|
|
||||||
whyChooseUs: {
|
|
||||||
title: "",
|
|
||||||
subtitle: "",
|
|
||||||
description: "",
|
|
||||||
button: { label: "", href: "" },
|
|
||||||
features: [],
|
|
||||||
tags: [],
|
|
||||||
cta: { text: "", linkText: "", linkHref: "" },
|
|
||||||
},
|
|
||||||
activities: { cards: [] },
|
|
||||||
faq: {
|
|
||||||
title: "",
|
|
||||||
subtitle: "",
|
|
||||||
description: "",
|
|
||||||
image: "",
|
|
||||||
contact: { title: "", info: "" },
|
|
||||||
questions: [],
|
|
||||||
},
|
|
||||||
partners: {
|
|
||||||
title: "",
|
|
||||||
subtitle: "",
|
|
||||||
backgroundImage: "",
|
|
||||||
logos: [],
|
|
||||||
cta: { badge: "", text: "", linkText: "", linkHref: "" },
|
|
||||||
},
|
|
||||||
programs: {
|
|
||||||
title: "",
|
|
||||||
subtitle: "",
|
|
||||||
button: { label: "", href: "" },
|
|
||||||
card: {
|
|
||||||
pricePrefix: "from",
|
|
||||||
priceSuffix: "USD",
|
|
||||||
buttonLabel: "Camp Detail",
|
|
||||||
buttonHref: "/camp-profiles",
|
|
||||||
},
|
|
||||||
items: [],
|
items: [],
|
||||||
},
|
selectedBlogIds: [] // Array of manually selected blog IDs
|
||||||
newsletter: {
|
|
||||||
title: "",
|
|
||||||
subtitle: "",
|
|
||||||
description: "",
|
|
||||||
image: "",
|
|
||||||
decorativeImage: "",
|
|
||||||
button: { label: "", placeholder: "", href: "" },
|
|
||||||
},
|
|
||||||
latestPosts: {
|
|
||||||
title: "",
|
|
||||||
subtitle: "",
|
|
||||||
searchPlaceholder: "",
|
|
||||||
sidebarTitle: "",
|
|
||||||
blogPosts: [],
|
|
||||||
sidebarPosts: [],
|
|
||||||
featuredCard: { image: "", title: "", description: "" },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// -------------------- Admin --------------------
|
// Admin: Xem trang quản lý
|
||||||
|
|
||||||
exports.index = async (req, res) => {
|
exports.index = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
let data = await getHomeData();
|
let data = await getHomeData();
|
||||||
|
const defaults = getDefaultHomeData();
|
||||||
if (!data || Object.keys(data).length === 0) {
|
|
||||||
data = getDefaultHomeData();
|
// Merge dữ liệu mặc định cho tất cả các phần
|
||||||
} else {
|
const sections = Object.keys(defaults);
|
||||||
// Merge minimal defaults to keep the view safe
|
sections.forEach(s => {
|
||||||
const defaults = getDefaultHomeData();
|
data[s] = data[s] || defaults[s];
|
||||||
data.hero = data.hero || defaults.hero;
|
});
|
||||||
data.about = data.about || defaults.about;
|
|
||||||
data.missionVision = data.missionVision || defaults.missionVision;
|
|
||||||
data.whyChooseUs = data.whyChooseUs || defaults.whyChooseUs;
|
|
||||||
data.activities = data.activities || defaults.activities;
|
|
||||||
data.faq = data.faq || defaults.faq;
|
|
||||||
data.partners = data.partners || defaults.partners;
|
|
||||||
data.programs = data.programs || defaults.programs;
|
|
||||||
data.newsletter = data.newsletter || defaults.newsletter;
|
|
||||||
data.latestPosts = data.latestPosts || defaults.latestPosts;
|
|
||||||
}
|
|
||||||
|
|
||||||
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||||
|
const backendUrl = process.env.BACKEND_URL || "http://localhost:3001";
|
||||||
|
|
||||||
|
// Lấy tất cả blog để chọn trong CMS
|
||||||
|
const allBlogs = await Blog.find({ status: "published" }).sort({ createdAt: -1 }).lean();
|
||||||
|
|
||||||
return res.render("admin/home/index", {
|
return res.render("admin/home/index", {
|
||||||
layout: "layouts/main",
|
layout: "layouts/main",
|
||||||
title: "Home Management",
|
title: "Home Management",
|
||||||
data,
|
data,
|
||||||
|
allBlogs,
|
||||||
frontendUrl,
|
frontendUrl,
|
||||||
|
backendUrl,
|
||||||
|
getFullImageUrl,
|
||||||
currentPath: req.path,
|
currentPath: req.path,
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
});
|
});
|
||||||
@@ -143,73 +61,32 @@ exports.index = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Admin: Cập nhật dữ liệu (tập trung vào achievements, partners; các phần khác giữ nguyên nếu không có dữ liệu mới)
|
||||||
exports.update = async (req, res) => {
|
exports.update = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const currentDoc = await getHomeDoc();
|
|
||||||
const currentData = currentDoc ? currentDoc.toObject() : {};
|
|
||||||
const updatedData = { ...currentData };
|
|
||||||
|
|
||||||
// Quick fields (Hero) from classic form fields
|
|
||||||
if (req.body.heroTitle || req.body.heroDescription || req.body.heroBackgroundImage) {
|
|
||||||
updatedData.hero = {
|
|
||||||
title: req.body.heroTitle || "",
|
|
||||||
description: req.body.heroDescription || "",
|
|
||||||
backgroundImage: req.body.heroBackgroundImage || "",
|
|
||||||
button: {
|
|
||||||
label: req.body.heroButtonLabel || "Book Your Adventure",
|
|
||||||
href: req.body.heroButtonHref || "/booking",
|
|
||||||
},
|
|
||||||
contactBox: {
|
|
||||||
welcomeText: req.body.heroContactWelcome || "",
|
|
||||||
phone: {
|
|
||||||
label: "Call us",
|
|
||||||
number: req.body.heroContactPhone || "",
|
|
||||||
href: req.body.heroContactPhone ? `tel:${req.body.heroContactPhone}` : "",
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
label: "Email",
|
|
||||||
address: req.body.heroContactEmail || "",
|
|
||||||
href: req.body.heroContactEmail ? `mailto:${req.body.heroContactEmail}` : "",
|
|
||||||
},
|
|
||||||
workingHours: {
|
|
||||||
label: "Working Hours",
|
|
||||||
hours: req.body.heroContactHours || "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle sections sent as JSON payloads
|
|
||||||
const sections = [
|
const sections = [
|
||||||
"hero",
|
"hero", "whyChooseUs", "visaSolutions", "visaCountries",
|
||||||
"about",
|
"testimonials", "videoGallery", "faq", "achievements",
|
||||||
"missionVision",
|
"partners", "blogPreview"
|
||||||
"whyChooseUs",
|
|
||||||
"activities",
|
|
||||||
"faq",
|
|
||||||
"partners",
|
|
||||||
"programs",
|
|
||||||
"newsletter",
|
|
||||||
"latestPosts",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let doc = await getHomeDoc();
|
||||||
|
if (!doc) {
|
||||||
|
doc = new Home({});
|
||||||
|
}
|
||||||
|
|
||||||
let hasChanges = false;
|
let hasChanges = false;
|
||||||
|
|
||||||
for (const section of sections) {
|
for (const section of sections) {
|
||||||
if (!req.body[section]) continue;
|
if (req.body[section]) {
|
||||||
|
try {
|
||||||
try {
|
const payload = JSON.parse(req.body[section]);
|
||||||
const newSectionData = JSON.parse(req.body[section]);
|
// Gán trực tiếp vào doc, Mongoose sẽ tự check schema
|
||||||
const currentSectionData = currentData?.[section];
|
doc[section] = payload;
|
||||||
|
doc.markModified(section);
|
||||||
if (JSON.stringify(newSectionData) !== JSON.stringify(currentSectionData)) {
|
|
||||||
updatedData[section] = newSectionData;
|
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Invalid JSON for ${section}:`, e);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error(`Error processing section "${section}":`, e);
|
|
||||||
req.flash("error_msg", `Invalid JSON for section "${section}": ${e.message}`);
|
|
||||||
return req.session.save(() => res.redirect("/admin/home"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,29 +95,75 @@ exports.update = async (req, res) => {
|
|||||||
return req.session.save(() => res.redirect("/admin/home"));
|
return req.session.save(() => res.redirect("/admin/home"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentDoc?._id) {
|
await doc.save();
|
||||||
await Home.findByIdAndUpdate(currentDoc._id, updatedData, { new: true });
|
req.flash("success_msg", "Home page configuration has been updated!");
|
||||||
} else {
|
|
||||||
await Home.create(updatedData);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.flash("success_msg", "Home data updated successfully");
|
|
||||||
return req.session.save(() => res.redirect("/admin/home"));
|
return req.session.save(() => res.redirect("/admin/home"));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Home update error:", err);
|
console.error("Home update error:", err);
|
||||||
req.flash("error_msg", `Update error: ${err.message || "Unknown"}`);
|
req.flash("error_msg", `Update error: ${err.message}`);
|
||||||
return req.session.save(() => res.redirect("/admin/home"));
|
return req.session.save(() => res.redirect("/admin/home"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// -------------------- Public API --------------------
|
// Public API// API lấy danh sách blog cho CMS
|
||||||
|
exports.apiGetBlogs = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const blogs = await Blog.find({ status: "published" }).sort({ createdAt: -1 }).select("title slug featuredImage author publishedAt").lean();
|
||||||
|
res.json(blogs);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
exports.api = async (req, res) => {
|
exports.api = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const homeData = await getHomeData();
|
let data = await getHomeData();
|
||||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||||
const processedData = addBaseUrlToImages(homeData, baseUrl);
|
|
||||||
return res.json(processedData);
|
// === Xử lý Blog Preview động ===
|
||||||
|
const blogPreview = data.blogPreview || {};
|
||||||
|
let blogs = [];
|
||||||
|
|
||||||
|
// Nếu có chọn blog cụ thể
|
||||||
|
if (blogPreview.selectedBlogIds && blogPreview.selectedBlogIds.length > 0) {
|
||||||
|
blogs = await Blog.find({
|
||||||
|
_id: { $in: blogPreview.selectedBlogIds },
|
||||||
|
status: "published"
|
||||||
|
}).lean();
|
||||||
|
|
||||||
|
// Sắp xếp theo thứ tự đã chọn trong selectedBlogIds
|
||||||
|
blogs.sort((a, b) => {
|
||||||
|
return blogPreview.selectedBlogIds.indexOf(a._id.toString()) - blogPreview.selectedBlogIds.indexOf(b._id.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nếu không chọn hoặc chọn nhưng không đủ, lấy thêm 3 bài mới nhất (hoặc bù vào)
|
||||||
|
if (blogs.length === 0) {
|
||||||
|
blogs = await Blog.find({ status: "published" })
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.limit(3)
|
||||||
|
.lean();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map dữ liệu blog sang format mà frontend mong đợi
|
||||||
|
blogPreview.items = blogs.map(blog => ({
|
||||||
|
title: blog.title,
|
||||||
|
excerpt: blog.excerpt,
|
||||||
|
category: blog.category && blog.category[0] ? blog.category[0] : "Visa",
|
||||||
|
date: blog.publishedAt || blog.createdAt,
|
||||||
|
author: {
|
||||||
|
name: blog.author || "Admin",
|
||||||
|
avatar: "" // Frontend đang tự xử lý hoặc dùng logo hệ thống
|
||||||
|
},
|
||||||
|
comments: blog.commentsCount || 0,
|
||||||
|
link: `/blog/${blog.slug}`,
|
||||||
|
thumbnail: blog.featuredImage
|
||||||
|
}));
|
||||||
|
|
||||||
|
data.blogPreview = blogPreview;
|
||||||
|
// ===============================
|
||||||
|
|
||||||
|
const processed = addBaseUrlToImages(data, baseUrl);
|
||||||
|
return res.json(processed);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Home API error:", err);
|
console.error("Home API error:", err);
|
||||||
return res.status(500).json({ error: "Error loading home data" });
|
return res.status(500).json({ error: "Error loading home data" });
|
||||||
|
|||||||
@@ -132,36 +132,11 @@
|
|||||||
{
|
{
|
||||||
"label": "Blog",
|
"label": "Blog",
|
||||||
"slug": "blog",
|
"slug": "blog",
|
||||||
"href": "#",
|
"href": "/blog",
|
||||||
"type": "internal",
|
"type": "internal",
|
||||||
"order": 5,
|
"order": 5,
|
||||||
"isActive": true,
|
"isActive": true,
|
||||||
"children": [
|
"children": []
|
||||||
{
|
|
||||||
"label": "Blog Grid",
|
|
||||||
"slug": "blog-grid",
|
|
||||||
"href": "/blog-grid",
|
|
||||||
"type": "internal",
|
|
||||||
"order": 1,
|
|
||||||
"isActive": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Blog Standard",
|
|
||||||
"slug": "blog-standard",
|
|
||||||
"href": "/blog",
|
|
||||||
"type": "internal",
|
|
||||||
"order": 2,
|
|
||||||
"isActive": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Blog Details",
|
|
||||||
"slug": "blog-details",
|
|
||||||
"href": "/blog-details",
|
|
||||||
"type": "internal",
|
|
||||||
"order": 3,
|
|
||||||
"isActive": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Contact Us",
|
"label": "Contact Us",
|
||||||
|
|||||||
@@ -238,54 +238,50 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"partners": {
|
"partners": {
|
||||||
"heading": "Our Trusted Partners",
|
"visaConsultancy": {
|
||||||
"items": [
|
"heading": "Our Achievements & Awards",
|
||||||
{
|
"items": [
|
||||||
"name": "Best Visa Consultancy",
|
{
|
||||||
"logo": "/assets/img/home-1/feature/icon-1.png",
|
"name": "Best Visa Consultancy",
|
||||||
"year": "2025"
|
"icon": "/assets/img/home-1/feature/icon-1.png",
|
||||||
},
|
"year": "2025"
|
||||||
{
|
},
|
||||||
"name": "Visa Success Award",
|
{
|
||||||
"logo": "/assets/img/home-1/feature/icon-2.png",
|
"name": "Visa Success Award",
|
||||||
"year": "2025"
|
"icon": "/assets/img/home-1/feature/icon-2.png",
|
||||||
},
|
"year": "2025"
|
||||||
{
|
},
|
||||||
"name": "Innovation Award",
|
{
|
||||||
"logo": "/assets/img/home-1/feature/icon-3.png",
|
"name": "Innovation Award",
|
||||||
"year": "2025"
|
"icon": "/assets/img/home-1/feature/icon-3.png",
|
||||||
},
|
"year": "2025"
|
||||||
{
|
},
|
||||||
"name": "Global Education Partner",
|
{
|
||||||
"logo": "/assets/img/home-1/feature/icon-4.png",
|
"name": "Global Education Partner",
|
||||||
"year": "2025"
|
"icon": "/assets/img/home-1/feature/icon-4.png",
|
||||||
},
|
"year": "2025"
|
||||||
{
|
}
|
||||||
"name": "University Partner 1",
|
]
|
||||||
"logo": "/assets/img/home-1/brand/01.png",
|
},
|
||||||
"year": "2025"
|
"brands": {
|
||||||
},
|
"items": [
|
||||||
{
|
{
|
||||||
"name": "University Partner 2",
|
"logo": "/assets/img/home-1/brand/01.png"
|
||||||
"logo": "/assets/img/home-1/brand/02.png",
|
},
|
||||||
"year": "2025"
|
{
|
||||||
},
|
"logo": "/assets/img/home-1/brand/02.png"
|
||||||
{
|
},
|
||||||
"name": "University Partner 3",
|
{
|
||||||
"logo": "/assets/img/home-1/brand/03.png",
|
"logo": "/assets/img/home-1/brand/03.png"
|
||||||
"year": "2025"
|
},
|
||||||
},
|
{
|
||||||
{
|
"logo": "/assets/img/home-1/brand/04.png"
|
||||||
"name": "University Partner 4",
|
},
|
||||||
"logo": "/assets/img/home-1/brand/04.png",
|
{
|
||||||
"year": "2025"
|
"logo": "/assets/img/home-1/brand/05.png"
|
||||||
},
|
}
|
||||||
{
|
]
|
||||||
"name": "University Partner 5",
|
}
|
||||||
"logo": "/assets/img/home-1/brand/05.png",
|
|
||||||
"year": "2025"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"blogPreview": {
|
"blogPreview": {
|
||||||
"heading": "Latest Insights & Updates",
|
"heading": "Latest Insights & Updates",
|
||||||
|
|||||||
@@ -156,19 +156,40 @@ const AchievementsSchema = new Schema(
|
|||||||
{ _id: false },
|
{ _id: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
const PartnerItemSchema = new Schema(
|
const VisaConsultancyItemSchema = new Schema(
|
||||||
{
|
{
|
||||||
name: { type: String, default: "" },
|
name: { type: String, default: "" },
|
||||||
logo: { type: String, default: "" },
|
icon: { type: String, default: "" },
|
||||||
year: { type: String, default: "" },
|
year: { type: String, default: "" },
|
||||||
},
|
},
|
||||||
{ _id: false },
|
{ _id: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const VisaConsultancySchema = new Schema(
|
||||||
|
{
|
||||||
|
items: { type: [VisaConsultancyItemSchema], default: [] },
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
const BrandItemSchema = new Schema(
|
||||||
|
{
|
||||||
|
logo: { type: String, default: "" },
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
const BrandsSchema = new Schema(
|
||||||
|
{
|
||||||
|
items: { type: [BrandItemSchema], default: [] },
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
const PartnersSchema = new Schema(
|
const PartnersSchema = new Schema(
|
||||||
{
|
{
|
||||||
heading: { type: String, default: "" },
|
visaConsultancy: { type: VisaConsultancySchema, default: () => ({}) },
|
||||||
items: { type: [PartnerItemSchema], default: [] },
|
brands: { type: BrandsSchema, default: () => ({}) },
|
||||||
},
|
},
|
||||||
{ _id: false },
|
{ _id: false },
|
||||||
);
|
);
|
||||||
@@ -196,6 +217,7 @@ const BlogPreviewSchema = new Schema(
|
|||||||
subheading: { 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' }],
|
||||||
},
|
},
|
||||||
{ _id: false },
|
{ _id: false },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ router.get("/dashboard", ensureAuthenticated, dashboardController.getDashboard);
|
|||||||
// Home
|
// Home
|
||||||
router.get("/home", ensureAuthenticated, homeController.index);
|
router.get("/home", ensureAuthenticated, homeController.index);
|
||||||
router.post("/home/update", ensureAuthenticated, homeController.update);
|
router.post("/home/update", ensureAuthenticated, homeController.update);
|
||||||
|
router.get("/home/api/blogs", ensureAuthenticated, homeController.apiGetBlogs);
|
||||||
|
|
||||||
// Middleware chuẩn hóa code
|
// Middleware chuẩn hóa code
|
||||||
router.param("code", (req, res, next, code) => {
|
router.param("code", (req, res, next, code) => {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
122
views/admin/home/sections/achievements.ejs
Normal file
122
views/admin/home/sections/achievements.ejs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<!-- Achievements Tab -->
|
||||||
|
<div class="tab-pane fade" id="achievements" role="tabpanel">
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<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-chart-pie me-2"></i>General Information
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<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="achievementsHeading"
|
||||||
|
value="<%= data.achievements?.heading || '' %>"
|
||||||
|
placeholder="e.g., Our Achievements in Numbers"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-medium">Subheading</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="achievementsSubheading"
|
||||||
|
value="<%= data.achievements?.subheading || '' %>"
|
||||||
|
placeholder="e.g., Did You Know"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Achievement Items -->
|
||||||
|
<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-list-ul me-2"></i>Achievement Items (Fixed 4 Items)
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" id="achievementItemsContainer">
|
||||||
|
<% for(let i=0; i<4; i++) {
|
||||||
|
const item = (data.achievements?.items && data.achievements.items[i]) || {};
|
||||||
|
%>
|
||||||
|
<div class="card mb-3 bg-light border achievement-item">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h6 class="card-title fw-bold mb-0">Achievement #<%= i + 1 %></h6>
|
||||||
|
</div>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label fw-medium">Value (Number)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control achievement-value"
|
||||||
|
value="<%= item.value || '' %>"
|
||||||
|
placeholder="e.g., 95"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label fw-medium">Suffix</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control achievement-suffix"
|
||||||
|
value="<%= item.suffix || '' %>"
|
||||||
|
placeholder="e.g., %"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-medium">Label</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control achievement-label"
|
||||||
|
value="<%= item.label || '' %>"
|
||||||
|
placeholder="e.g., Visa Success Rate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label fw-medium">Description</label>
|
||||||
|
<textarea
|
||||||
|
class="form-control achievement-description"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Short description of this achievement"
|
||||||
|
><%= item.description || '' %></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Đăng ký scraper cho phần achievements
|
||||||
|
window.homeScrapers = window.homeScrapers || {};
|
||||||
|
window.homeScrapers.achievements = function() {
|
||||||
|
const items = [];
|
||||||
|
document.querySelectorAll('.achievement-item').forEach(el => {
|
||||||
|
items.push({
|
||||||
|
value: el.querySelector('.achievement-value').value,
|
||||||
|
suffix: el.querySelector('.achievement-suffix').value,
|
||||||
|
label: el.querySelector('.achievement-label').value,
|
||||||
|
description: el.querySelector('.achievement-description').value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
heading: document.getElementById('achievementsHeading').value,
|
||||||
|
subheading: document.getElementById('achievementsSubheading').value,
|
||||||
|
items: items
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
175
views/admin/home/sections/blogPreview.ejs
Normal file
175
views/admin/home/sections/blogPreview.ejs
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<!-- Blog Preview Tab -->
|
||||||
|
<div class="tab-pane fade" id="blogpreview" role="tabpanel">
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card border shadow-sm">
|
||||||
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>Basic Information & Blog Selection
|
||||||
|
</h6>
|
||||||
|
<span class="badge bg-info text-dark">CMS will automatically fetch the 3 latest posts if no specific blog is
|
||||||
|
selected.</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<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="blogPreviewHeading"
|
||||||
|
value="<%= data.blogPreview?.heading || '' %>" placeholder="e.g., Latest Insights & Updates" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-medium">Subheading</label>
|
||||||
|
<input type="text" class="form-control" id="blogPreviewSubheading"
|
||||||
|
value="<%= data.blogPreview?.subheading || '' %>" placeholder="e.g., Visa Tips & Guides" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12 mt-4">
|
||||||
|
<label class="form-label fw-bold"><i class="fas fa-check-square me-2"></i>Select Featured Blogs (Direct
|
||||||
|
from Blog Module)</label>
|
||||||
|
<p class="text-muted small mb-3">Select blog posts to prioritize on the home page. If none are selected,
|
||||||
|
the system will use the 3 latest posts.</p>
|
||||||
|
<div class="row g-3 blog-selector-container"
|
||||||
|
style="max-height: 400px; overflow-y: auto; border: 1px solid #eee; padding: 15px; border-radius: 8px;">
|
||||||
|
<% if (allBlogs && allBlogs.length> 0) { %>
|
||||||
|
<% allBlogs.forEach(blog=> {
|
||||||
|
const isSelected = data.blogPreview?.selectedBlogIds && data.blogPreview.selectedBlogIds.some(id =>
|
||||||
|
id.toString() === blog._id.toString());
|
||||||
|
%>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card h-100 blog-select-card <%= isSelected ? 'border-primary bg-light' : '' %>"
|
||||||
|
onclick="toggleBlogSelection(this, '<%= blog._id %>')"
|
||||||
|
style="cursor: pointer; transition: all 0.2s;">
|
||||||
|
<div class="position-absolute top-0 end-0 m-2">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input blog-checkbox" type="checkbox" value="<%= blog._id %>"
|
||||||
|
<%=isSelected ? 'checked' : '' %> onclick="event.stopPropagation();
|
||||||
|
handleCheckboxChange(this)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
src="<%= blog.featuredImage ? getFullImageUrl(blog.featuredImage, backendUrl) : '/assets/img/placeholder.jpg' %>"
|
||||||
|
class="card-img-top" style="height: 210px; object-fit: cover;">
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<h6 class="card-title small fw-bold mb-1" title="<%= blog.title %>"
|
||||||
|
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; height: 2.6em; line-height: 1.3em;">
|
||||||
|
<%= blog.title %>
|
||||||
|
</h6>
|
||||||
|
<p class="card-text tiny text-muted mb-0">
|
||||||
|
<%= blog.publishedAt ? new Date(blog.publishedAt).toLocaleDateString('vi-VN') : '' %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="col-12 text-center py-4">
|
||||||
|
<p class="text-muted">No published blogs found. Please create some blogs first.</p>
|
||||||
|
<a href="/admin/blog/create" class="btn btn-sm btn-outline-primary">Create Blog</a>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CTA Button -->
|
||||||
|
<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-mouse-pointer me-2"></i>CTA Button
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<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="blogPreviewCtaLabel"
|
||||||
|
value="<%= data.blogPreview?.ctaButton?.label || '' %>" placeholder="e.g., View All Articles" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-medium">Link</label>
|
||||||
|
<input type="text" class="form-control" id="blogPreviewCtaHref"
|
||||||
|
value="<%= data.blogPreview?.ctaButton?.href || '' %>" placeholder="/blog" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleBlogSelection(card, blogId) {
|
||||||
|
const checkbox = card.querySelector('.blog-checkbox');
|
||||||
|
const isChecking = !checkbox.checked;
|
||||||
|
|
||||||
|
if (isChecking) {
|
||||||
|
const checkedCount = document.querySelectorAll('.blog-checkbox:checked').length;
|
||||||
|
if (checkedCount >= 3) {
|
||||||
|
alert('You can only select up to 3 blogs.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkbox.checked = isChecking;
|
||||||
|
handleCheckboxUpdate(card, checkbox.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCheckboxChange(checkbox) {
|
||||||
|
if (checkbox.checked) {
|
||||||
|
const checkedCount = document.querySelectorAll('.blog-checkbox:checked').length;
|
||||||
|
if (checkedCount > 3) {
|
||||||
|
checkbox.checked = false;
|
||||||
|
alert('You can only select up to 3 blogs.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const card = checkbox.closest('.blog-select-card');
|
||||||
|
handleCheckboxUpdate(card, checkbox.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCheckboxUpdate(card, isChecked) {
|
||||||
|
if (isChecked) {
|
||||||
|
card.classList.add('border-primary', 'bg-light');
|
||||||
|
} else {
|
||||||
|
card.classList.remove('border-primary', 'bg-light');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Đăng ký scraper cho Blog Preview
|
||||||
|
window.homeScrapers = window.homeScrapers || {};
|
||||||
|
window.homeScrapers.blogPreview = () => {
|
||||||
|
const selectedIds = [];
|
||||||
|
document.querySelectorAll('.blog-checkbox:checked').forEach(cb => {
|
||||||
|
selectedIds.push(cb.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Chúng ta vẫn giữ cấu trúc cũ cho items nếu muốn preview offline,
|
||||||
|
// nhưng server sẽ ưu tiên dùng selectedBlogIds để populate dữ liệu mới nhất.
|
||||||
|
return {
|
||||||
|
heading: document.getElementById('blogPreviewHeading').value,
|
||||||
|
subheading: document.getElementById('blogPreviewSubheading').value,
|
||||||
|
ctaButton: {
|
||||||
|
label: document.getElementById('blogPreviewCtaLabel').value,
|
||||||
|
href: document.getElementById('blogPreviewCtaHref').value
|
||||||
|
},
|
||||||
|
selectedBlogIds: selectedIds,
|
||||||
|
items: [] // Server side will handle full items content
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tiny {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-select-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
124
views/admin/home/sections/faq.ejs
Normal file
124
views/admin/home/sections/faq.ejs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<!-- FAQ Tab -->
|
||||||
|
<div class="tab-pane fade" id="faq" role="tabpanel">
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<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-info-circle me-2"></i>Basic Information
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FAQ Items -->
|
||||||
|
<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-question-circle me-2"></i>FAQ Items
|
||||||
|
</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>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="faqQuestion_<%= index %>"
|
||||||
|
value="<%= item.question || '' %>"
|
||||||
|
placeholder="Enter question"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label fw-medium">Answer</label>
|
||||||
|
<textarea
|
||||||
|
class="form-control"
|
||||||
|
id="faqAnswer_<%= index %>"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Enter answer"
|
||||||
|
><%= item.answer || '' %></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }); %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CTA Button -->
|
||||||
|
<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-mouse-pointer me-2"></i>CTA Button
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
157
views/admin/home/sections/hero.ejs
Normal file
157
views/admin/home/sections/hero.ejs
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<!-- Hero Tab -->
|
||||||
|
<div class="tab-pane fade show active" id="hero" role="tabpanel">
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<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-info-circle me-2"></i>Basic Information
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</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">
|
||||||
|
<div class="card border shadow-sm">
|
||||||
|
<div class="card-header bg-white">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-mouse-pointer me-2"></i>Primary 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="heroPrimaryButtonLabel"
|
||||||
|
value="<%= data.hero?.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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Secondary Button -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card border shadow-sm">
|
||||||
|
<div class="card-header bg-white">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-mouse-pointer me-2"></i>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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
150
views/admin/home/sections/partners.ejs
Normal file
150
views/admin/home/sections/partners.ejs
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
<!-- Partners Tab -->
|
||||||
|
<div class="tab-pane fade" id="partners" role="tabpanel">
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Visa Consultancy Awards -->
|
||||||
|
<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-award me-2"></i>Awards & Certifications (Fixed 4 Items)
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="visaConsultancyContainer">
|
||||||
|
<% for(let i=0; i<4; i++) {
|
||||||
|
const item = (data.partners?.visaConsultancy?.items && data.partners.visaConsultancy.items[i]) || {};
|
||||||
|
%>
|
||||||
|
<div class="card mb-3 bg-light border visa-item">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h6 class="card-title fw-bold mb-0">Award #<%= i + 1 %></h6>
|
||||||
|
</div>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label fw-medium">Award Name</label>
|
||||||
|
<input type="text" class="form-control visa-name" value="<%= item.name || '' %>" placeholder="Award Name" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label fw-medium">Year</label>
|
||||||
|
<input type="text" class="form-control visa-year" value="<%= item.year || '' %>" placeholder="e.g., 2025" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label fw-medium">Icon / Logo</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<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"
|
||||||
|
data-target-input="visaIcon_<%= i %>" data-image-type="home">
|
||||||
|
<i class="fas fa-upload me-1"></i>Upload
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 preview-container">
|
||||||
|
<img src="<%= item.icon || '' %>" class="img-thumbnail <%= item.icon ? '' : 'd-none' %>" style="height: 60px; object-fit: contain;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Brand Logos -->
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card border shadow-sm">
|
||||||
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-images me-2"></i>Brand Partner Logos (Slider)
|
||||||
|
</h6>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="addBrandPartnerItem()">
|
||||||
|
<i class="fas fa-plus me-1"></i>Add Logo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="brandPartnersContainer" class="row g-3">
|
||||||
|
<% (data.partners?.brands?.items || []).forEach(function(item, index) { %>
|
||||||
|
<div class="col-md-4 brand-partner-item">
|
||||||
|
<div class="card border bg-light h-100">
|
||||||
|
<div class="card-body py-2">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<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()">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<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"
|
||||||
|
data-target-input="brandLogo_<%= index %>" data-image-type="home">
|
||||||
|
<i class="fas fa-upload"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<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;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }); %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Thu thập dữ liệu partners
|
||||||
|
window.homeScrapers = window.homeScrapers || {};
|
||||||
|
window.homeScrapers.partners = function() {
|
||||||
|
const visaItems = [];
|
||||||
|
document.querySelectorAll('.visa-item').forEach(el => {
|
||||||
|
visaItems.push({
|
||||||
|
name: el.querySelector('.visa-name').value,
|
||||||
|
year: el.querySelector('.visa-year').value,
|
||||||
|
icon: el.querySelector('.visa-icon').value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const brandItems = [];
|
||||||
|
document.querySelectorAll('.brand-partner-item').forEach(el => {
|
||||||
|
const logo = el.querySelector('.brand-logo-input').value;
|
||||||
|
if (logo) brandItems.push({ logo: logo });
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
visaConsultancy: { items: visaItems },
|
||||||
|
brands: { items: brandItems }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function addBrandPartnerItem() {
|
||||||
|
const container = document.getElementById('brandPartnersContainer');
|
||||||
|
const id = 'brandLogo_' + Date.now();
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'col-md-4 brand-partner-item';
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="card border bg-light h-100">
|
||||||
|
<div class="card-body py-2">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<span class="small fw-bold">New Brand Logo</span>
|
||||||
|
<button type="button" class="btn btn-link text-danger p-0" onclick="this.closest('.brand-partner-item').remove()">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input type="text" class="form-control brand-logo-input" id="${id}">
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="${id}" data-image-type="home">
|
||||||
|
<i class="fas fa-upload"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-center preview-container">
|
||||||
|
<img src="" class="img-thumbnail d-none" style="height: 50px; object-fit: contain;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
container.appendChild(div);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
159
views/admin/home/sections/testimonials.ejs
Normal file
159
views/admin/home/sections/testimonials.ejs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<!-- Testimonials Tab -->
|
||||||
|
<div class="tab-pane fade" id="testimonials" role="tabpanel">
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<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-info-circle me-2"></i>Basic Information
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</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=..."
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<i class="fas fa-upload me-1"></i>Upload
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Testimonial Items -->
|
||||||
|
<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-comments me-2"></i>Testimonials
|
||||||
|
</h6>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<i class="fas fa-upload me-1"></i>Upload
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }); %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
67
views/admin/home/sections/videoGallery.ejs
Normal file
67
views/admin/home/sections/videoGallery.ejs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<!-- Video Gallery Tab -->
|
||||||
|
<div class="tab-pane fade" id="videogallery" role="tabpanel">
|
||||||
|
<div class="row g-4">
|
||||||
|
<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-video me-2"></i>Video Gallery
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
163
views/admin/home/sections/visaCountries.ejs
Normal file
163
views/admin/home/sections/visaCountries.ejs
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<!-- Visa Countries Tab -->
|
||||||
|
<div class="tab-pane fade" id="visacountries" role="tabpanel">
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<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-info-circle me-2"></i>Basic Information
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Countries -->
|
||||||
|
<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
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<% (data.visaCountries?.countries || []).forEach(function(country, index) { %>
|
||||||
|
<div class="card mb-3 bg-light border">
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-medium">Flag 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"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }); %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CTA Button -->
|
||||||
|
<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-mouse-pointer me-2"></i>CTA Button
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
100
views/admin/home/sections/visaSolutions.ejs
Normal file
100
views/admin/home/sections/visaSolutions.ejs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<!-- Visa Solutions Tab -->
|
||||||
|
<div class="tab-pane fade" id="visasolutions" role="tabpanel">
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<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-info-circle me-2"></i>Basic Information
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Services Items -->
|
||||||
|
<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-list-ul me-2"></i>Visa Solutions Items
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }); %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
169
views/admin/home/sections/whyChooseUs.ejs
Normal file
169
views/admin/home/sections/whyChooseUs.ejs
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<!-- Why Choose Us Tab -->
|
||||||
|
<div class="tab-pane fade" id="whychooseus" role="tabpanel">
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<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-info-circle me-2"></i>Basic Information
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<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"
|
||||||
|
value="<%= data.whyChooseUs?.heading || '' %>"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Items -->
|
||||||
|
<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-list-ul me-2"></i>Why Choose Us Items
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<% (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>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }); %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Features -->
|
||||||
|
<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-star me-2"></i>Features
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<% (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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<% }); %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CTA Button -->
|
||||||
|
<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-mouse-pointer me-2"></i>CTA Button
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -178,8 +178,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align: center; margin-bottom: 30px;">
|
<div style="text-align: center; margin-bottom: 30px;">
|
||||||
<h4 style="color: #b8b76a; font-weight: 600; margin-bottom: 10px;">CMS Management System</h4>
|
<h4 style="color: var(--primary-color); font-weight: 600; margin-bottom: 10px;">CMS Management System</h4>
|
||||||
<p style="color: #666; font-size: 14px;">Welcome to Content Management System</p>
|
<p style="color: var(--text-color); font-size: 14px;">Welcome to Content Management System</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form action="/auth/login" method="POST" class="login-form">
|
<form action="/auth/login" method="POST" class="login-form">
|
||||||
|
|||||||
Reference in New Issue
Block a user