forked from UKSOURCE/cms.hailearning.edu.vn
feat: Add initial CMS for home page content management, including sections for hero, testimonials, partners, and more.
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 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 = () => ({
|
||||
hero: {
|
||||
title: "",
|
||||
description: "",
|
||||
backgroundImage: "",
|
||||
button: { label: "Book Your Adventure", href: "/booking" },
|
||||
contactBox: {
|
||||
welcomeText: "",
|
||||
phone: { label: "Call us", number: "", href: "" },
|
||||
email: { label: "Email", address: "", href: "" },
|
||||
workingHours: { label: "Working Hours", hours: "" },
|
||||
},
|
||||
},
|
||||
about: {
|
||||
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",
|
||||
},
|
||||
hero: { title: "", subtitle: "", description: "", backgroundImage: "", videoUrl: "", primaryButton: {}, secondaryButton: {} },
|
||||
whyChooseUs: { heading: "", subheading: "", description: "", items: [], features: [], ctaButton: {} },
|
||||
visaSolutions: { heading: "", subheading: "", items: [] },
|
||||
visaCountries: { heading: "", subheading: "", description: "", countries: [], ctaButton: {} },
|
||||
testimonials: { heading: "", subheading: "", videoUrl: "", videoThumbnail: "", items: [] },
|
||||
videoGallery: { heading: "", videoUrl: "", thumbnail: "" },
|
||||
faq: { heading: "", subheading: "", description: "", ctaButton: {}, items: [] },
|
||||
achievements: { heading: "", subheading: "", items: [] },
|
||||
partners: { visaConsultancy: { items: [] }, brands: { items: [] } },
|
||||
blogPreview: {
|
||||
heading: "Latest Insights & Updates",
|
||||
subheading: "Visa Tips & Guides",
|
||||
ctaButton: { label: "View All Articles", href: "/blog" },
|
||||
items: [],
|
||||
},
|
||||
newsletter: {
|
||||
title: "",
|
||||
subtitle: "",
|
||||
description: "",
|
||||
image: "",
|
||||
decorativeImage: "",
|
||||
button: { label: "", placeholder: "", href: "" },
|
||||
},
|
||||
latestPosts: {
|
||||
title: "",
|
||||
subtitle: "",
|
||||
searchPlaceholder: "",
|
||||
sidebarTitle: "",
|
||||
blogPosts: [],
|
||||
sidebarPosts: [],
|
||||
featuredCard: { image: "", title: "", description: "" },
|
||||
selectedBlogIds: [] // Array of manually selected blog IDs
|
||||
},
|
||||
});
|
||||
|
||||
// -------------------- Admin --------------------
|
||||
|
||||
// Admin: Xem trang quản lý
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
let data = await getHomeData();
|
||||
const defaults = getDefaultHomeData();
|
||||
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
data = getDefaultHomeData();
|
||||
} else {
|
||||
// Merge minimal defaults to keep the view safe
|
||||
const defaults = getDefaultHomeData();
|
||||
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;
|
||||
}
|
||||
// Merge dữ liệu mặc định cho tất cả các phần
|
||||
const sections = Object.keys(defaults);
|
||||
sections.forEach(s => {
|
||||
data[s] = data[s] || defaults[s];
|
||||
});
|
||||
|
||||
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", {
|
||||
layout: "layouts/main",
|
||||
title: "Home Management",
|
||||
data,
|
||||
allBlogs,
|
||||
frontendUrl,
|
||||
backendUrl,
|
||||
getFullImageUrl,
|
||||
currentPath: req.path,
|
||||
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) => {
|
||||
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 = [
|
||||
"hero",
|
||||
"about",
|
||||
"missionVision",
|
||||
"whyChooseUs",
|
||||
"activities",
|
||||
"faq",
|
||||
"partners",
|
||||
"programs",
|
||||
"newsletter",
|
||||
"latestPosts",
|
||||
"hero", "whyChooseUs", "visaSolutions", "visaCountries",
|
||||
"testimonials", "videoGallery", "faq", "achievements",
|
||||
"partners", "blogPreview"
|
||||
];
|
||||
|
||||
let doc = await getHomeDoc();
|
||||
if (!doc) {
|
||||
doc = new Home({});
|
||||
}
|
||||
|
||||
let hasChanges = false;
|
||||
|
||||
for (const section of sections) {
|
||||
if (!req.body[section]) continue;
|
||||
|
||||
try {
|
||||
const newSectionData = JSON.parse(req.body[section]);
|
||||
const currentSectionData = currentData?.[section];
|
||||
|
||||
if (JSON.stringify(newSectionData) !== JSON.stringify(currentSectionData)) {
|
||||
updatedData[section] = newSectionData;
|
||||
if (req.body[section]) {
|
||||
try {
|
||||
const payload = JSON.parse(req.body[section]);
|
||||
// Gán trực tiếp vào doc, Mongoose sẽ tự check schema
|
||||
doc[section] = payload;
|
||||
doc.markModified(section);
|
||||
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"));
|
||||
}
|
||||
|
||||
if (currentDoc?._id) {
|
||||
await Home.findByIdAndUpdate(currentDoc._id, updatedData, { new: true });
|
||||
} else {
|
||||
await Home.create(updatedData);
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Home data updated successfully");
|
||||
await doc.save();
|
||||
req.flash("success_msg", "Home page configuration has been updated!");
|
||||
return req.session.save(() => res.redirect("/admin/home"));
|
||||
} catch (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"));
|
||||
}
|
||||
};
|
||||
|
||||
// -------------------- 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) => {
|
||||
try {
|
||||
const homeData = await getHomeData();
|
||||
let data = await getHomeData();
|
||||
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) {
|
||||
console.error("Home API error:", err);
|
||||
return res.status(500).json({ error: "Error loading home data" });
|
||||
|
||||
@@ -132,36 +132,11 @@
|
||||
{
|
||||
"label": "Blog",
|
||||
"slug": "blog",
|
||||
"href": "#",
|
||||
"href": "/blog",
|
||||
"type": "internal",
|
||||
"order": 5,
|
||||
"isActive": true,
|
||||
"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
|
||||
}
|
||||
]
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"label": "Contact Us",
|
||||
|
||||
@@ -238,54 +238,50 @@
|
||||
]
|
||||
},
|
||||
"partners": {
|
||||
"heading": "Our Trusted Partners",
|
||||
"items": [
|
||||
{
|
||||
"name": "Best Visa Consultancy",
|
||||
"logo": "/assets/img/home-1/feature/icon-1.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"name": "Visa Success Award",
|
||||
"logo": "/assets/img/home-1/feature/icon-2.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"name": "Innovation Award",
|
||||
"logo": "/assets/img/home-1/feature/icon-3.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"name": "Global Education Partner",
|
||||
"logo": "/assets/img/home-1/feature/icon-4.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"name": "University Partner 1",
|
||||
"logo": "/assets/img/home-1/brand/01.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"name": "University Partner 2",
|
||||
"logo": "/assets/img/home-1/brand/02.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"name": "University Partner 3",
|
||||
"logo": "/assets/img/home-1/brand/03.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"name": "University Partner 4",
|
||||
"logo": "/assets/img/home-1/brand/04.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"name": "University Partner 5",
|
||||
"logo": "/assets/img/home-1/brand/05.png",
|
||||
"year": "2025"
|
||||
}
|
||||
]
|
||||
"visaConsultancy": {
|
||||
"heading": "Our Achievements & Awards",
|
||||
"items": [
|
||||
{
|
||||
"name": "Best Visa Consultancy",
|
||||
"icon": "/assets/img/home-1/feature/icon-1.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"name": "Visa Success Award",
|
||||
"icon": "/assets/img/home-1/feature/icon-2.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"name": "Innovation Award",
|
||||
"icon": "/assets/img/home-1/feature/icon-3.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"name": "Global Education Partner",
|
||||
"icon": "/assets/img/home-1/feature/icon-4.png",
|
||||
"year": "2025"
|
||||
}
|
||||
]
|
||||
},
|
||||
"brands": {
|
||||
"items": [
|
||||
{
|
||||
"logo": "/assets/img/home-1/brand/01.png"
|
||||
},
|
||||
{
|
||||
"logo": "/assets/img/home-1/brand/02.png"
|
||||
},
|
||||
{
|
||||
"logo": "/assets/img/home-1/brand/03.png"
|
||||
},
|
||||
{
|
||||
"logo": "/assets/img/home-1/brand/04.png"
|
||||
},
|
||||
{
|
||||
"logo": "/assets/img/home-1/brand/05.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"blogPreview": {
|
||||
"heading": "Latest Insights & Updates",
|
||||
|
||||
@@ -156,19 +156,40 @@ const AchievementsSchema = new Schema(
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const PartnerItemSchema = new Schema(
|
||||
const VisaConsultancyItemSchema = new Schema(
|
||||
{
|
||||
name: { type: String, default: "" },
|
||||
logo: { type: String, default: "" },
|
||||
icon: { type: String, default: "" },
|
||||
year: { type: String, default: "" },
|
||||
},
|
||||
{ _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(
|
||||
{
|
||||
heading: { type: String, default: "" },
|
||||
items: { type: [PartnerItemSchema], default: [] },
|
||||
visaConsultancy: { type: VisaConsultancySchema, default: () => ({}) },
|
||||
brands: { type: BrandsSchema, default: () => ({}) },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
@@ -196,6 +217,7 @@ const BlogPreviewSchema = new Schema(
|
||||
subheading: { type: String, default: "" },
|
||||
ctaButton: { type: LinkSchema, default: () => ({}) },
|
||||
items: { type: [BlogPreviewItemSchema], default: [] },
|
||||
selectedBlogIds: [{ type: Schema.Types.ObjectId, ref: 'Blog' }],
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
@@ -37,6 +37,7 @@ router.get("/dashboard", ensureAuthenticated, dashboardController.getDashboard);
|
||||
// Home
|
||||
router.get("/home", ensureAuthenticated, homeController.index);
|
||||
router.post("/home/update", ensureAuthenticated, homeController.update);
|
||||
router.get("/home/api/blogs", ensureAuthenticated, homeController.apiGetBlogs);
|
||||
|
||||
// Middleware chuẩn hóa 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 style="text-align: center; margin-bottom: 30px;">
|
||||
<h4 style="color: #b8b76a; font-weight: 600; margin-bottom: 10px;">CMS Management System</h4>
|
||||
<p style="color: #666; font-size: 14px;">Welcome to Content Management System</p>
|
||||
<h4 style="color: var(--primary-color); font-weight: 600; margin-bottom: 10px;">CMS Management System</h4>
|
||||
<p style="color: var(--text-color); font-size: 14px;">Welcome to Content Management System</p>
|
||||
</div>
|
||||
|
||||
<form action="/auth/login" method="POST" class="login-form">
|
||||
|
||||
Reference in New Issue
Block a user