feat: Add initial CMS for home page content management, including sections for hero, testimonials, partners, and more.

This commit is contained in:
Wini_Fy
2026-02-05 15:55:27 +07:00
parent b4891101e7
commit de50d554c9
17 changed files with 1681 additions and 2887 deletions

View File

@@ -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) { // Merge dữ liệu mặc định cho tất cả các phần
data = getDefaultHomeData(); const sections = Object.keys(defaults);
} else { sections.forEach(s => {
// Merge minimal defaults to keep the view safe data[s] = data[s] || defaults[s];
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;
}
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" });

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 },
); );

View File

@@ -37,6 +37,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

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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">