Merge branch 'fea/dat-07042026-float-social-buttons' of https://gits.techvanguard.vn/UKSOURCE/cms.hailearning.edu.vn into fea/nhat-06042026-menu-management

This commit is contained in:
Đỗ Minh Nhật
2026-04-11 03:40:37 +07:00
59 changed files with 4098 additions and 547 deletions

View File

@@ -27,6 +27,7 @@
<input type="hidden" name="achievements" id="achievementsJson" />
<input type="hidden" name="partners" id="partnersJson" />
<input type="hidden" name="blogPreview" id="blogPreviewJson" />
<input type="hidden" name="floatingContact" id="floatingContactJson" />
<!-- Navigation Tabs -->
<div class="card shadow-sm border-0 mb-4">
@@ -82,6 +83,11 @@
<i class="fas fa-blog me-2"></i>Blog Preview
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#floatingcontact" role="tab">
<i class="fas fa-comment-dots me-2"></i>Floating Contact
</a>
</li>
</ul>
</div>
@@ -94,9 +100,10 @@
<%- include('sections/testimonials') %>
<%- include('sections/videoGallery') %>
<%- include('sections/faq') %>
<%- include('sections/achievements') %>
<%- include('sections/partners') %>
<%- include('sections/achievements') %>
<%- include('sections/partners') %>
<%- include('sections/blogPreview') %>
<%- include('sections/floatingContact') %>
</div>
</div>
</div>
@@ -118,9 +125,14 @@
</div>
<!-- Image upload input -->
<input type="file" id="directImageUpload" style="display: none" />
<input
type="file"
id="directImageUpload"
style="display: none"
accept="image/*,.png,.jpg,.jpeg,.gif,.webp,.svg" />
<input type="hidden" id="currentImageType" name="imageType" />
<input type="hidden" id="currentTargetInput" name="targetInput" />
<input type="hidden" id="currentResizePreset" name="resizePreset" />
<script>
/**
@@ -132,43 +144,231 @@
* <\/script>
*/
window.homeScrapers = window.homeScrapers || {};
const pendingImageUploads = new Map();
const pendingPreviewUrls = new Map();
document.addEventListener("DOMContentLoaded", function () {
const form = document.querySelector("form");
if (form) {
form.addEventListener("submit", function (e) {
form.addEventListener("submit", async function (e) {
if (form.dataset.submitting === "true") {
return;
}
e.preventDefault();
if (typeof form.reportValidity === "function" && !form.reportValidity()) {
return;
}
console.log("Form submitting, collecting data from scrapers...");
// Tự động thu gom dữ liệu từ các section đã đăng ký
Object.keys(window.homeScrapers).forEach(section => {
const input = document.getElementById(section + 'Json');
if (input) {
try {
const data = window.homeScrapers[section]();
console.log(`- Collected data for [${section}]:`, data);
input.value = JSON.stringify(data);
} catch (err) {
console.error(`Error scraping section [${section}]:`, err);
}
}
});
try {
await flushPendingImageUploads();
// Để form tự submit tự nhiên sau khi đã điền xong các hidden inputs
// Tự động thu gom dữ liệu từ các section đã đăng ký
Object.keys(window.homeScrapers).forEach(section => {
const input = document.getElementById(section + 'Json');
if (input) {
try {
const data = window.homeScrapers[section]();
console.log(`- Collected data for [${section}]:`, data);
input.value = JSON.stringify(data);
} catch (err) {
console.error(`Error scraping section [${section}]:`, err);
}
}
});
form.dataset.submitting = "true";
form.submit();
} catch (error) {
console.error("Error during deferred image uploads:", error);
showToast("Error", error.message || "Failed to upload pending images", "error");
}
});
}
// Khởi tạo các nút upload ảnh (dùng chung cho toàn bộ các section)
initImageUploads();
initHomeCharacterCounters(document);
initImagePreviewFallbacks(document);
});
// --- UTILITIES (Dùng chung) ---
function extractHtmlErrorMessage(html) {
if (!html) {
return "";
}
const preMatch = html.match(/<pre>([\s\S]*?)<\/pre>/i);
const titleMatch = html.match(/<title>([\s\S]*?)<\/title>/i);
const rawMessage = (preMatch && preMatch[1]) || (titleMatch && titleMatch[1]) || html;
const decoded = rawMessage
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&amp;/g, "&");
return decoded.replace(/\s+/g, " ").trim();
}
function findImagePreview(input) {
if (!input) {
return null;
}
return (
input.closest(".input-group")?.nextElementSibling?.querySelector("img") ||
input.parentElement?.nextElementSibling?.querySelector("img") ||
null
);
}
function clearImagePreviewError(previewImg) {
if (!previewImg?.parentElement) {
return;
}
previewImg.parentElement.querySelectorAll(".image-preview-missing").forEach((node) => node.remove());
}
function bindImagePreviewFallback(previewImg) {
if (!previewImg || previewImg.dataset.previewFallbackBound === "true") {
return;
}
previewImg.dataset.previewFallbackBound = "true";
previewImg.addEventListener("error", () => {
previewImg.classList.add("d-none");
previewImg.removeAttribute("src");
if (!previewImg.parentElement?.querySelector(".image-preview-missing")) {
const note = document.createElement("small");
note.className = "text-warning d-block mt-2 image-preview-missing";
note.textContent = "Preview unavailable: current image path could not be loaded.";
previewImg.parentElement?.appendChild(note);
}
});
}
function initImagePreviewFallbacks(scope = document) {
scope.querySelectorAll("img.img-thumbnail").forEach((previewImg) => {
bindImagePreviewFallback(previewImg);
if (previewImg.complete && previewImg.getAttribute("src") && previewImg.naturalWidth === 0) {
previewImg.dispatchEvent(new Event("error"));
}
});
}
function revokePendingPreview(targetInput) {
const previewUrl = pendingPreviewUrls.get(targetInput);
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
pendingPreviewUrls.delete(targetInput);
}
}
function isFloatingContactTargetInput(targetInput) {
return typeof targetInput === "string" && targetInput.startsWith("floatingContact");
}
async function persistFloatingContactDraft() {
const scraper = window.homeScrapers && window.homeScrapers.floatingContact;
if (typeof scraper !== "function") {
throw new Error("Floating contact scraper is not available");
}
const response = await fetch("/admin/home/floating-contact/update", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
floatingContact: scraper(),
}),
});
const result = await response.json().catch(() => null);
if (!response.ok || !result?.success) {
throw new Error(result?.error || "Failed to save floating contact changes");
}
return result;
}
async function uploadPendingImage(targetInput, uploadConfig) {
const input = document.getElementById(targetInput);
if (!input || !uploadConfig?.file) {
pendingImageUploads.delete(targetInput);
revokePendingPreview(targetInput);
return;
}
const formData = new FormData();
formData.append("image", uploadConfig.file);
const query = new URLSearchParams({ imageType: uploadConfig.imageType });
if (uploadConfig.resizePreset) {
query.set("resizePreset", uploadConfig.resizePreset);
}
const response = await fetch(`/admin/upload/image?${query.toString()}`, {
method: "POST",
body: formData,
});
const rawResponse = await response.text();
let result = null;
try {
result = rawResponse ? JSON.parse(rawResponse) : null;
} catch (parseError) {
result = null;
}
if (!response.ok) {
throw new Error(
result?.error ||
extractHtmlErrorMessage(rawResponse) ||
`Upload failed with status ${response.status}`,
);
}
if (!result?.success || !result.path) {
throw new Error(result?.error || "Upload failed");
}
input.value = result.path;
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
const previewImg = findImagePreview(input);
if (previewImg) {
clearImagePreviewError(previewImg);
bindImagePreviewFallback(previewImg);
previewImg.src = new URL(result.path, window.location.origin).toString();
previewImg.classList.remove("d-none");
}
pendingImageUploads.delete(targetInput);
revokePendingPreview(targetInput);
}
async function flushPendingImageUploads() {
for (const [targetInput, uploadConfig] of pendingImageUploads.entries()) {
await uploadPendingImage(targetInput, uploadConfig);
}
}
function initImageUploads() {
document.addEventListener("click", function (e) {
const btn = e.target.closest(".btn-upload-image");
if (btn) {
document.getElementById("currentImageType").value = btn.dataset.imageType;
document.getElementById("currentTargetInput").value = btn.dataset.targetInput;
document.getElementById("currentResizePreset").value = btn.dataset.resizePreset || "";
document.getElementById("directImageUpload").click();
}
});
@@ -184,28 +384,45 @@
const file = this.files[0];
const imageType = document.getElementById("currentImageType").value;
const targetInput = document.getElementById("currentTargetInput").value;
const resizePreset = document.getElementById("currentResizePreset").value;
const allowedExtensions = /\.(jpe?g|png|gif|webp|svg)$/i;
if (!(file.type.startsWith("image/") || allowedExtensions.test(file.name))) {
showToast("Error", "Only image files are allowed", "error");
this.value = "";
return;
}
try {
const formData = new FormData();
formData.append("image", file);
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, { method: "POST", body: formData });
const result = await response.json();
if (result.success && result.path) {
const input = document.getElementById(targetInput);
if (input) {
input.value = result.path;
// Cập nhật preview nếu có img ngay sau input group
const previewImg = input.closest('.input-group')?.nextElementSibling?.querySelector('img');
if (previewImg) {
previewImg.src = result.path;
previewImg.classList.remove('d-none');
}
}
showToast("Success", "Image uploaded successfully", "success");
} else {
throw new Error(result.error || "Upload failed");
const input = document.getElementById(targetInput);
if (!input) {
throw new Error("Target image field not found");
}
revokePendingPreview(targetInput);
const previewUrl = URL.createObjectURL(file);
pendingPreviewUrls.set(targetInput, previewUrl);
const previewImg = findImagePreview(input);
if (previewImg) {
clearImagePreviewError(previewImg);
bindImagePreviewFallback(previewImg);
previewImg.src = previewUrl;
previewImg.classList.remove("d-none");
}
if (isFloatingContactTargetInput(targetInput)) {
pendingImageUploads.delete(targetInput);
await uploadPendingImage(targetInput, { file, imageType, resizePreset });
await persistFloatingContactDraft();
showToast("Success", "Image uploaded and saved immediately.", "success");
this.value = "";
return;
}
pendingImageUploads.set(targetInput, { file, imageType, resizePreset });
showToast("Ready", "Image selected. It will be uploaded when you save changes.", "info");
} catch (error) {
showToast("Error", "Upload failed: " + error.message, "error");
}
@@ -228,4 +445,12 @@
new bootstrap.Toast(toast, { autohide: true, delay: 3000 }).show();
toast.addEventListener("hidden.bs.toast", () => toast.remove());
}
</script>
function initHomeCharacterCounters(scope = document) {
scope.querySelectorAll(".home-limit-counter").forEach((node) => node.remove());
if (window.AdminFormHelpers) {
window.AdminFormHelpers.refresh(scope);
}
}
</script>

View File

@@ -25,12 +25,14 @@
<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" />
value="<%= data.blogPreview?.heading || '' %>" placeholder="e.g., Latest Insights & Updates"
maxlength="64" data-maxlength="64" />
</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" />
value="<%= data.blogPreview?.subheading || '' %>" placeholder="e.g., Visa Tips & Guides"
maxlength="40" data-maxlength="40" />
</div>
<div class="col-md-12 mt-4">
@@ -97,12 +99,14 @@
<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" />
value="<%= data.blogPreview?.ctaButton?.label || '' %>" placeholder="e.g., View All Articles"
maxlength="32" data-maxlength="32" />
</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" />
value="<%= data.blogPreview?.ctaButton?.href || '' %>" placeholder="/blog"
maxlength="255" data-maxlength="255" />
</div>
</div>
</div>
@@ -185,4 +189,4 @@
transform: translateY(-3px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
</style>
</style>

View File

@@ -27,17 +27,17 @@
<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" />
placeholder="e.g., Got Questions? We've Got Answers" maxlength="64" data-maxlength="64" />
</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" />
placeholder="e.g., Visa FAQs" maxlength="40" data-maxlength="40" />
</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>
placeholder="Enter description" maxlength="220" data-maxlength="220"><%= data.faq?.description || '' %></textarea>
</div>
</div>
</div>
@@ -82,6 +82,8 @@
class="form-control"
id="faqQuestion_<%= index %>"
value="<%= item.question || '' %>"
maxlength="120"
data-maxlength="120"
placeholder="Enter question"
/>
</div>
@@ -91,6 +93,8 @@
class="form-control"
id="faqAnswer_<%= index %>"
rows="3"
maxlength="320"
data-maxlength="320"
placeholder="Enter answer"
><%= item.answer || '' %></textarea>
</div>
@@ -115,12 +119,12 @@
<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" />
placeholder="e.g., contact us" maxlength="32" data-maxlength="32" />
</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" />
placeholder="/contact" maxlength="255" data-maxlength="255" />
</div>
</div>
</div>
@@ -161,4 +165,4 @@
enabled
};
};
</script>
</script>

View File

@@ -0,0 +1,483 @@
<!-- Floating Contact Tab -->
<div class="tab-pane fade" id="floatingcontact" role="tabpanel">
<div class="row g-4">
<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-sliders-h me-2"></i>Widget Settings
</h6>
<span class="badge bg-light text-dark border">Homepage floating contact widget</span>
</div>
<div class="card-body">
<div class="row g-4 align-items-start">
<div class="col-12">
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 p-3 border rounded-3 bg-light-subtle">
<div>
<div class="fw-semibold">Widget visibility</div>
<small class="text-muted">Enable or disable the floating contact widget on the homepage.</small>
</div>
<div class="form-check form-switch m-0">
<input class="form-check-input" type="checkbox" id="floatingContactEnabled"
<%= data.floatingContact?.enabled !== false ? 'checked' : '' %> />
<label class="form-check-label fw-medium" for="floatingContactEnabled">
Enable floating widget
</label>
</div>
</div>
</div>
<div class="col-lg-8">
<label class="form-label fw-medium">Panel Title</label>
<input type="text" class="form-control" id="floatingContactPanelTitle"
value="<%= data.floatingContact?.panelTitle || '' %>"
placeholder="How can we help you today?"
maxlength="72"
data-maxlength="72" />
<small class="text-muted">Maximum 72 characters to keep the header from breaking the widget layout.</small>
</div>
<div class="col-lg-4">
<label class="form-label fw-medium">Brand Alt Text</label>
<input type="text" class="form-control" id="floatingContactBrandAlt"
value="<%= data.floatingContact?.brand?.imageAlt || 'HAI Learning' %>" placeholder="HAI Learning"
maxlength="60"
data-maxlength="60" />
<small class="text-muted d-block mt-2">Used for accessibility and fallback image descriptions.</small>
</div>
<div class="col-lg-6">
<label class="form-label fw-medium">Brand Image</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="floatingContactBrandImage"
value="<%= data.floatingContact?.brand?.imageSrc || '' %>" placeholder="/assets/img/logo/black-logo.svg"
maxlength="255" data-maxlength="255" />
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="floatingContactBrandImage" data-image-type="home/floating-contact"
data-resize-preset="floatingContactBrandImage">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<div class="mt-2">
<img
src="<%= data.floatingContact?.brand?.imageSrc ? getFullImageUrl(data.floatingContact.brand.imageSrc, backendUrl) : '' %>"
class="img-thumbnail <%= data.floatingContact?.brand?.imageSrc ? '' : 'd-none' %>"
id="floatingContactBrandPreview"
style="height: 120px; width: 120px; object-fit: contain; background: #fff;"
alt="Brand preview" />
</div>
<small class="text-muted d-block mt-2">Displayed at roughly 35x35px. Raster uploads are normalized to 104x104 WebP to match the homepage widget.</small>
</div>
<div class="col-lg-6">
<label class="form-label fw-medium">Fallback Trigger Image</label>
<div class="input-group mb-2">
<input
type="text"
class="form-control"
id="floatingContactTriggerImage"
value="<%= data.floatingContact?.trigger?.imageSrc || '' %>"
placeholder="/uploads/home/floating-contact/floating-trigger-icon.webp"
maxlength="255" data-maxlength="255" />
<button
type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="floatingContactTriggerImage"
data-image-type="home/floating-contact"
data-resize-preset="floatingContactTriggerIcon">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<div class="mt-2">
<img
src="<%= data.floatingContact?.trigger?.imageSrc ? getFullImageUrl(data.floatingContact.trigger.imageSrc, backendUrl) : '' %>"
class="img-thumbnail <%= data.floatingContact?.trigger?.imageSrc ? '' : 'd-none' %>"
id="floatingContactTriggerPreview"
style="height: 120px; width: 120px; object-fit: contain; background: #fff;"
alt="Trigger icon preview" />
</div>
<input type="hidden" id="floatingContactTriggerIconFallback"
value="<%= data.floatingContact?.trigger?.icon || 'fa-comments' %>" />
<small class="text-muted d-block mt-2">Displayed at roughly 26x26px. Shown only when the trigger slideshow cannot use the brand image and action icons. Raster uploads are normalized to 96x96 WebP.</small>
</div>
</div>
</div>
</div>
</div>
<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">
<div>
<h6 class="mb-0">
<i class="fas fa-list me-2"></i>Contact Actions
</h6>
<small class="text-muted">Add, remove, and drag to reorder floating contact actions.</small>
</div>
<button type="button" class="btn btn-sm btn-primary" id="addFloatingContactActionBtn">
<i class="fas fa-plus me-1"></i>Add Action
</button>
</div>
<div class="card-body">
<div id="floatingContactActionsContainer" class="d-grid gap-3"></div>
</div>
</div>
</div>
</div>
</div>
<div
id="floatingContactConfig"
data-cms-base-url="<%= backendUrl.replace(/\/$/, '') %>"
hidden
></div>
<script id="floatingContactInitialData" type="application/json"><%- JSON.stringify(data.floatingContact || {}) %></script>
<script>
window.homeScrapers = window.homeScrapers || {};
const floatingContactConfig = document.getElementById("floatingContactConfig");
const floatingContactCmsBaseUrl = floatingContactConfig?.dataset.cmsBaseUrl || "";
const normalizeFloatingContactPublicPath = (value) => {
const raw = (value || "").trim();
if (!raw) {
return "";
}
const knownPrefixes = ["/uploads/", "/assets/", "/img/"];
for (const prefix of knownPrefixes) {
const absolutePrefix = `${floatingContactCmsBaseUrl}${prefix}`;
if (raw.startsWith(absolutePrefix)) {
return raw.slice(floatingContactCmsBaseUrl.length);
}
}
for (const prefix of knownPrefixes) {
const index = raw.indexOf(prefix);
if (index >= 0) {
return raw.slice(index);
}
}
if (raw.startsWith("/")) {
return raw;
}
return `/${raw}`;
};
const resolveFloatingContactImageUrl = (value) => {
const normalized = normalizeFloatingContactPublicPath(value);
return normalized ? `${floatingContactCmsBaseUrl}${normalized}` : "";
};
const escapeFloatingContactHtml = (value) =>
String(value || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
document.addEventListener("DOMContentLoaded", function () {
const initialDataElement = document.getElementById("floatingContactInitialData");
const initialData = initialDataElement?.textContent
? JSON.parse(initialDataElement.textContent)
: {};
const container = document.getElementById("floatingContactActionsContainer");
const addBtn = document.getElementById("addFloatingContactActionBtn");
const brandInput = document.getElementById("floatingContactBrandImage");
const brandPreview = document.getElementById("floatingContactBrandPreview");
const triggerInput = document.getElementById("floatingContactTriggerImage");
const triggerPreview = document.getElementById("floatingContactTriggerPreview");
if (!container || !addBtn) {
return;
}
const createActionId = () => `floating-action-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const getActionDefaults = (platform) => {
if (platform === "facebook") {
return {
platform: "facebook",
enabled: true,
label: "Message us on Facebook",
subtitle: "facebook.com/hailearning.edu.vn",
href: "https://www.facebook.com/hailearning.edu.vn/",
iconImage: "",
iconType: "iconClass",
iconClass: "fa-brands fa-facebook-messenger",
iconText: "",
};
}
if (platform === "zalo") {
return {
platform: "zalo",
enabled: true,
label: "Message us on Zalo",
subtitle: "zalo.me/84961834040",
href: "https://zalo.me/84961834040",
iconImage: "",
iconType: "iconText",
iconClass: "",
iconText: "Zalo",
};
}
return {
platform: "custom",
enabled: true,
label: "",
subtitle: "",
href: "",
iconImage: "",
iconType: "iconClass",
iconClass: "fa-solid fa-comment-dots",
iconText: "",
};
};
const bindImageField = (input, preview) => {
if (!input || !preview) {
return;
}
input.value = normalizeFloatingContactPublicPath(input.value);
if (input.value) {
preview.src = resolveFloatingContactImageUrl(input.value);
preview.classList.remove("d-none");
}
if (input.dataset.previewBound === "true") {
return;
}
input.addEventListener("input", () => {
const value = normalizeFloatingContactPublicPath(input.value);
input.value = value;
if (!value) {
preview.classList.add("d-none");
preview.removeAttribute("src");
return;
}
preview.src = resolveFloatingContactImageUrl(value);
preview.classList.remove("d-none");
});
input.dataset.previewBound = "true";
};
const createActionCard = (action = {}) => {
const defaults = {
id: createActionId(),
...getActionDefaults(action.platform || "custom"),
...action,
};
const normalizedIconImage = normalizeFloatingContactPublicPath(defaults.iconImage || "");
const iconInputId = `floatingContactActionIconImage-${defaults.id}`;
const iconPreviewId = `${iconInputId}-preview`;
const wrapper = document.createElement("div");
wrapper.className = "card border floating-contact-action-item";
wrapper.innerHTML = `
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary floating-contact-action-handle" title="Drag to reorder">
<i class="fas fa-grip-vertical"></i>
</button>
<strong class="floating-contact-action-title">Action</strong>
</div>
<button type="button" class="btn btn-sm btn-outline-danger floating-contact-remove-action">
<i class="fas fa-trash-alt me-1"></i>Remove
</button>
</div>
<div class="card-body">
<input type="hidden" class="floating-contact-action-id" value="${escapeFloatingContactHtml(defaults.id)}" />
<input type="hidden" class="floating-contact-action-legacy-icon-type" value="${escapeFloatingContactHtml(defaults.iconType)}" />
<input type="hidden" class="floating-contact-action-legacy-icon-class" value="${escapeFloatingContactHtml(defaults.iconClass)}" />
<input type="hidden" class="floating-contact-action-legacy-icon-text" value="${escapeFloatingContactHtml(defaults.iconText)}" />
<div class="row g-3 align-items-start">
<div class="col-lg-3 col-md-6">
<label class="form-label fw-medium">Platform</label>
<select class="form-select floating-contact-action-platform">
<option value="facebook" ${defaults.platform === "facebook" ? "selected" : ""}>Facebook</option>
<option value="zalo" ${defaults.platform === "zalo" ? "selected" : ""}>Zalo</option>
<option value="custom" ${defaults.platform === "custom" ? "selected" : ""}>Custom</option>
</select>
</div>
<div class="col-lg-5 col-md-6">
<label class="form-label fw-medium">Label</label>
<input type="text" class="form-control floating-contact-action-label" value="${escapeFloatingContactHtml(defaults.label)}" placeholder="Message us on Facebook" maxlength="48" data-maxlength="48" />
</div>
<div class="col-lg-4 col-md-8">
<label class="form-label fw-medium">Subtitle</label>
<input type="text" class="form-control floating-contact-action-subtitle" value="${escapeFloatingContactHtml(defaults.subtitle)}" placeholder="facebook.com/hailearning.edu.vn" maxlength="48" data-maxlength="48" />
</div>
<div class="col-lg-3 col-md-4">
<label class="form-label fw-medium d-block">Status</label>
<div class="form-check form-switch mt-2">
<input class="form-check-input floating-contact-action-enabled" type="checkbox" ${defaults.enabled !== false ? "checked" : ""}>
<label class="form-check-label fw-medium">Enabled</label>
</div>
</div>
<div class="col-lg-9 col-md-8">
<label class="form-label fw-medium">Link</label>
<input type="text" class="form-control floating-contact-action-href" value="${escapeFloatingContactHtml(defaults.href)}" placeholder="https://example.com" maxlength="255" data-maxlength="255" />
</div>
<div class="col-12">
<label class="form-label fw-medium">Icon Image</label>
<div class="input-group mb-2">
<input type="text" class="form-control floating-contact-action-icon-image" id="${escapeFloatingContactHtml(iconInputId)}" value="${escapeFloatingContactHtml(normalizedIconImage)}" placeholder="/uploads/home/floating-contact/floating-action-icon.webp" maxlength="255" data-maxlength="255" />
<button
type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="${escapeFloatingContactHtml(iconInputId)}"
data-image-type="home/floating-contact"
data-resize-preset="floatingContactActionIcon">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<div class="mt-2">
<img
src="${normalizedIconImage ? escapeFloatingContactHtml(resolveFloatingContactImageUrl(normalizedIconImage)) : ""}"
class="img-thumbnail floating-contact-action-icon-preview ${normalizedIconImage ? "" : "d-none"}"
id="${escapeFloatingContactHtml(iconPreviewId)}"
style="height: 120px; width: 120px; object-fit: contain; background: #fff;"
alt="Action icon preview" />
</div>
<small class="text-muted d-block mt-2">Raster uploads are normalized to 84x84 WebP. SVG files remain unchanged.</small>
</div>
</div>
</div>
`;
return wrapper;
};
const updateActionCardTitles = () => {
container.querySelectorAll(".floating-contact-action-item").forEach((item, index) => {
const title = item.querySelector(".floating-contact-action-title");
if (title) {
title.textContent = `Action ${index + 1}`;
}
});
};
const bindActionCard = (card) => {
if (typeof initHomeCharacterCounters === "function") {
initHomeCharacterCounters(card);
}
bindImageField(
card.querySelector(".floating-contact-action-icon-image"),
card.querySelector(".floating-contact-action-icon-preview"),
);
};
const maybeApplyPlatformDefaults = (item) => {
const platform = item.querySelector(".floating-contact-action-platform")?.value || "custom";
const defaults = getActionDefaults(platform);
item.querySelector(".floating-contact-action-label").value =
item.querySelector(".floating-contact-action-label").value || defaults.label;
item.querySelector(".floating-contact-action-subtitle").value =
item.querySelector(".floating-contact-action-subtitle").value || defaults.subtitle;
item.querySelector(".floating-contact-action-href").value =
item.querySelector(".floating-contact-action-href").value || defaults.href;
item.querySelector(".floating-contact-action-legacy-icon-type").value = defaults.iconType;
item.querySelector(".floating-contact-action-legacy-icon-class").value = defaults.iconClass;
item.querySelector(".floating-contact-action-legacy-icon-text").value = defaults.iconText;
};
const renderInitialActions = () => {
const actions = Array.isArray(initialData.actions) && initialData.actions.length > 0
? initialData.actions
: [getActionDefaults("facebook"), getActionDefaults("zalo")];
actions.forEach((action) => {
const card = createActionCard(action);
container.appendChild(card);
bindActionCard(card);
});
updateActionCardTitles();
};
addBtn.addEventListener("click", () => {
const card = createActionCard(getActionDefaults("custom"));
container.appendChild(card);
bindActionCard(card);
updateActionCardTitles();
});
container.addEventListener("click", (event) => {
const removeBtn = event.target.closest(".floating-contact-remove-action");
if (!removeBtn) {
return;
}
const card = removeBtn.closest(".floating-contact-action-item");
if (!card) {
return;
}
card.remove();
updateActionCardTitles();
});
container.addEventListener("change", (event) => {
const card = event.target.closest(".floating-contact-action-item");
if (!card) {
return;
}
if (event.target.matches(".floating-contact-action-platform")) {
maybeApplyPlatformDefaults(card);
}
});
bindImageField(brandInput, brandPreview);
bindImageField(triggerInput, triggerPreview);
if (window.Sortable) {
window.Sortable.create(container, {
animation: 150,
handle: ".floating-contact-action-handle",
onEnd: updateActionCardTitles,
});
}
renderInitialActions();
});
window.homeScrapers.floatingContact = () => {
const actions = Array.from(document.querySelectorAll(".floating-contact-action-item")).map((item, index) => ({
id: item.querySelector(".floating-contact-action-id")?.value || "",
platform: item.querySelector(".floating-contact-action-platform")?.value || "custom",
enabled: !!item.querySelector(".floating-contact-action-enabled")?.checked,
label: item.querySelector(".floating-contact-action-label")?.value?.trim() || "",
subtitle: item.querySelector(".floating-contact-action-subtitle")?.value?.trim() || "",
href: item.querySelector(".floating-contact-action-href")?.value?.trim() || "",
iconImage: normalizeFloatingContactPublicPath(item.querySelector(".floating-contact-action-icon-image")?.value?.trim() || ""),
iconType: item.querySelector(".floating-contact-action-legacy-icon-type")?.value || "iconClass",
iconClass: item.querySelector(".floating-contact-action-legacy-icon-class")?.value?.trim() || "",
iconText: item.querySelector(".floating-contact-action-legacy-icon-text")?.value?.trim() || "",
order: index + 1,
}));
return {
enabled: !!document.getElementById("floatingContactEnabled")?.checked,
panelTitle: document.getElementById("floatingContactPanelTitle")?.value?.trim() || "",
brand: {
imageSrc: normalizeFloatingContactPublicPath(document.getElementById("floatingContactBrandImage")?.value?.trim() || ""),
imageAlt: document.getElementById("floatingContactBrandAlt")?.value?.trim() || "",
},
trigger: {
imageSrc: normalizeFloatingContactPublicPath(document.getElementById("floatingContactTriggerImage")?.value?.trim() || ""),
icon: document.getElementById("floatingContactTriggerIconFallback")?.value?.trim() || "fa-comments",
},
actions,
};
};
</script>

View File

@@ -1,7 +1,6 @@
<!-- Hero Tab -->
<div class="tab-pane fade show active" id="hero" role="tabpanel">
<div class="row g-4">
<!-- Background Image (section-level) -->
<div class="col-md-12">
<div class="card border shadow-sm mb-1">
<div class="card-header bg-white d-flex justify-content-center align-items-center">
@@ -15,16 +14,20 @@
<div class="card border shadow-sm mb-3">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-image me-2"></i>Hero Background
<i class="fas fa-layer-group me-2"></i>Hero Carousel Setup
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Background Image</label>
<div class="row g-3 align-items-start">
<div class="col-lg-6">
<label class="form-label fw-medium">Fallback Background Image</label>
<small class="text-muted d-block mb-1">
Optional fallback. The hero desktop frame currently displays approximately 1512x544px, so upload a landscape image of at least 1920x700px.
</small>
<div class="input-group mb-2">
<input type="text" class="form-control" id="heroBackgroundImage"
value="<%= data.hero?.backgroundImage || '' %>" placeholder="/assets/img/home-1/hero/bg.jpg" />
value="<%= data.hero?.backgroundImage || '' %>" placeholder="/uploads/home/hero-fallback.jpg"
maxlength="255" data-maxlength="255" />
<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
@@ -37,6 +40,17 @@
</div>
<% } %>
</div>
<div class="col-lg-6">
<div class="border rounded p-3 h-100 bg-light-subtle">
<div class="fw-semibold mb-2">Recommended content structure</div>
<ul class="small text-muted mb-0 ps-3">
<li>Use landscape slide images; prefer large images to fit the container.</li>
<li>Keep titles to 2-4 lines to avoid overflow on mobile.</li>
<li>Limit descriptions to 1-3 short sentences.</li>
<li>Both buttons should use internal links like <code>/contact</code>.</li>
</ul>
</div>
</div>
</div>
</div>
</div>
@@ -84,24 +98,28 @@
<div class="col-md-6">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="heroSlide_<%= index %>_title"
value="<%= slide.title || '' %>" placeholder="e.g., From Application to Visa" />
value="<%= slide.title || '' %>" placeholder="e.g., From Application to Visa"
maxlength="72" data-maxlength="72" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subtitle</label>
<small class="text-muted d-block mb-1">Currently not rendered on the frontend; kept for backward compatibility with existing data.</small>
<input type="text" class="form-control" id="heroSlide_<%= index %>_subtitle"
value="<%= slide.subtitle || '' %>" placeholder="e.g., Global Education Simplified" />
value="<%= slide.subtitle || '' %>" placeholder="e.g., Global Education Simplified"
maxlength="48" data-maxlength="48" />
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="heroSlide_<%= index %>_description" rows="3"
placeholder="Enter hero description"><%= slide.description || '' %></textarea>
placeholder="Enter hero description" maxlength="220" data-maxlength="220"><%= slide.description || '' %></textarea>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Hero Image</label>
<small class="text-muted d-block mb-1">Recommended size: 893x848px</small>
<label class="form-label fw-medium">Slide Background Image</label>
<small class="text-muted d-block mb-1">Recommended upload size is 1920x700px or larger.</small>
<div class="input-group mb-2">
<input type="text" class="form-control" id="heroSlide_<%= index %>_heroImage"
value="<%= slide.heroImage || '' %>" placeholder="/assets/img/home-1/hero/man.png" />
value="<%= slide.heroImage || '' %>" placeholder="/uploads/home/hero-slide-01.jpg"
maxlength="255" data-maxlength="255" />
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="heroSlide_<%= index %>_heroImage" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload
@@ -116,8 +134,10 @@
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Video URL</label>
<small class="text-muted d-block mb-1">The frontend currently does not render video in the hero. Kept only to preserve existing data.</small>
<input type="text" class="form-control" id="heroSlide_<%= index %>_videoUrl"
value="<%= slide.videoUrl || '' %>" placeholder="https://www.youtube.com/watch?v=..." />
value="<%= slide.videoUrl || '' %>" placeholder="Optional"
maxlength="255" data-maxlength="255" />
</div>
<!-- Primary Button -->
@@ -130,12 +150,14 @@
<div class="col-md-12">
<label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="heroSlide_<%= index %>_primaryLabel"
value="<%= slide.primaryButton?.label || '' %>" placeholder="e.g., Apply now" />
value="<%= slide.primaryButton?.label || '' %>" placeholder="e.g., Apply now"
maxlength="32" data-maxlength="32" />
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="heroSlide_<%= index %>_primaryHref"
value="<%= slide.primaryButton?.href || '' %>" placeholder="/contact" />
value="<%= slide.primaryButton?.href || '' %>" placeholder="/contact"
maxlength="255" data-maxlength="255" />
</div>
</div>
</div>
@@ -152,12 +174,14 @@
<label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="heroSlide_<%= index %>_secondaryLabel"
value="<%= slide.secondaryButton?.label || '' %>"
placeholder="e.g., Book Free Consultation" />
placeholder="e.g., Book Free Consultation"
maxlength="32" data-maxlength="32" />
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="heroSlide_<%= index %>_secondaryHref"
value="<%= slide.secondaryButton?.href || '' %>" placeholder="/contact" />
value="<%= slide.secondaryButton?.href || '' %>" placeholder="/contact"
maxlength="255" data-maxlength="255" />
</div>
</div>
</div>
@@ -295,6 +319,9 @@
container.appendChild(clone);
updateLabels();
if (typeof initHomeCharacterCounters === "function") {
initHomeCharacterCounters(clone);
}
});
container.addEventListener("click", (e) => {
@@ -319,5 +346,8 @@
// Initial normalization (in case indices rendered from server are not 0..n)
updateLabels();
if (typeof initHomeCharacterCounters === "function") {
initHomeCharacterCounters(container);
}
});
</script>
</script>

View File

@@ -19,6 +19,7 @@
</h6>
</div>
<div class="card-body">
<small class="text-muted d-block mb-3">Award icons on the homepage display at roughly 124x124px. Recommended uploads are square images or transparent logos at least 248x248px.</small>
<div id="visaConsultancyContainer">
<% for(let i=0; i<4; i++) { const item=(data.partners?.visaConsultancy?.items &&
data.partners.visaConsultancy.items[i]) || {}; %>
@@ -41,6 +42,7 @@
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Icon / Logo</label>
<small class="text-muted d-block mb-1">Recommended: a 248x248px square image or transparent logo for crisp rendering.</small>
<div class="input-group">
<input type="text" class="form-control visa-icon" id="visaIcon_<%= i %>"
value="<%= item.icon || '' %>" />
@@ -75,6 +77,7 @@
</button>
</div>
<div class="card-body">
<small class="text-muted d-block mb-3">Brand logos in the slider display at roughly 159x48px. Recommended assets are SVGs or horizontal transparent logos at least 320x96px.</small>
<div id="brandPartnersContainer" class="row g-3">
<% (data.partners?.brands?.items || []).forEach(function(item, index) { %>
<div class="col-md-4 brand-partner-item">
@@ -95,6 +98,7 @@
<i class="fas fa-upload"></i>
</button>
</div>
<small class="text-muted d-block mt-2">Recommended: SVG or a 320x96px horizontal image so the logo stays sharp in the slider.</small>
<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;">
@@ -154,11 +158,12 @@
</button>
</div>
<div class="input-group input-group-sm">
<input type="text" class="form-control brand-logo-input" id="${id}">
<input type="text" class="form-control brand-logo-input" id="${id}" maxlength="255" data-maxlength="255">
<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>
<small class="text-muted d-block mt-2">Recommended: SVG or a 320x96px horizontal image so the logo stays sharp in the slider.</small>
<div class="mt-2 text-center preview-container">
<img src="" class="img-thumbnail d-none" style="height: 50px; object-fit: contain;">
</div>
@@ -166,5 +171,8 @@
</div>
`;
container.appendChild(div);
if (typeof initHomeCharacterCounters === "function") {
initHomeCharacterCounters(div);
}
}
</script>
</script>

View File

@@ -23,23 +23,27 @@
<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" />
value="<%= data.testimonials?.heading || '' %>" placeholder="e.g., Student Reviews & Testimonials"
maxlength="64" data-maxlength="64" />
</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" />
value="<%= data.testimonials?.subheading || '' %>" placeholder="e.g., What Our Students Say"
maxlength="40" data-maxlength="40" />
</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=..." />
value="<%= data.testimonials?.videoUrl || '' %>" placeholder="https://www.youtube.com/watch?v=..."
maxlength="255" data-maxlength="255" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Video Thumbnail</label>
<small class="text-muted d-block mb-1">The desktop thumbnail frame is approximately 416x370px. Recommended upload size is at least 832x740px.</small>
<div class="input-group mb-2">
<input type="text" class="form-control" id="testimonialsVideoThumbnail"
value="<%= data.testimonials?.videoThumbnail || '' %>" />
value="<%= data.testimonials?.videoThumbnail || '' %>" maxlength="255" data-maxlength="255" />
<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
@@ -78,17 +82,20 @@
<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" />
value="<%= item.name || '' %>" placeholder="e.g., Sohel Tanvir"
maxlength="48" data-maxlength="48" />
</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" />
value="<%= item.role || '' %>" placeholder="e.g., Student"
maxlength="48" data-maxlength="48" />
</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" />
value="<%= item.country || '' %>" placeholder="e.g., Canada"
maxlength="48" data-maxlength="48" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Rating</label>
@@ -98,13 +105,14 @@
<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>
placeholder="Enter testimonial comment" maxlength="280" data-maxlength="280"><%= item.comment || '' %></textarea>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Avatar</label>
<small class="text-muted d-block mb-1">Avatars display at roughly 48x48px. Recommended square image sizes are 96x96px or 128x128px.</small>
<div class="input-group mb-2">
<input type="text" class="form-control" id="testimonialsAvatar_<%= index %>"
value="<%= item.avatar || '' %>" />
value="<%= item.avatar || '' %>" maxlength="255" data-maxlength="255" />
<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
@@ -200,6 +208,9 @@
container.appendChild(clone);
updateLabels();
if (typeof initHomeCharacterCounters === "function") {
initHomeCharacterCounters(clone);
}
});
container.addEventListener("click", (e) => {
@@ -219,5 +230,8 @@
});
updateLabels();
if (typeof initHomeCharacterCounters === "function") {
initHomeCharacterCounters(container);
}
});
</script>
</script>

View File

@@ -23,18 +23,22 @@
<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" />
value="<%= data.videoGallery?.heading || '' %>" placeholder="e.g., VIDEO PLAY GALLERY"
maxlength="32" data-maxlength="32" data-maxwords="4" />
<small class="text-muted d-block mt-1">Limit this title to 4 words and 32 characters so it stays readable on the homepage.</small>
</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" />
value="<%= data.videoGallery?.videoUrl || '' %>" placeholder="https://example.com/video.mp4"
maxlength="255" data-maxlength="255" />
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Thumbnail Image</label>
<small class="text-muted d-block mb-1">If no video is provided, this image fills a desktop area of about 1552x906px. Recommended minimum upload: 1920x1120px.</small>
<div class="input-group mb-2">
<input type="text" class="form-control" id="videoGalleryThumbnail"
value="<%= data.videoGallery?.thumbnail || '' %>" />
value="<%= data.videoGallery?.thumbnail || '' %>" maxlength="255" data-maxlength="255" />
<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
@@ -68,4 +72,4 @@
enabled
};
};
</script>
</script>

View File

@@ -23,17 +23,19 @@
<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" />
value="<%= data.visaCountries?.heading || '' %>" placeholder="e.g., Visa & VISAWAY Services To UK"
maxlength="88" data-maxlength="88" />
</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" />
value="<%= data.visaCountries?.subheading || '' %>" placeholder="e.g., UK. United Kingdom"
maxlength="56" data-maxlength="56" />
</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>
placeholder="Enter description" maxlength="240" data-maxlength="240"><%= data.visaCountries?.description || '' %></textarea>
</div>
</div>
</div>
@@ -61,18 +63,20 @@
<div class="col-md-6">
<label class="form-label fw-medium">Country Name</label>
<input type="text" class="form-control" id="visaCountriesName_0" value="<%= featured.name || '' %>"
placeholder="e.g., United Kingdom" />
placeholder="e.g., United Kingdom" maxlength="40" data-maxlength="40" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Country Code</label>
<input type="text" class="form-control" id="visaCountriesCode_0" value="<%= featured.code || '' %>"
placeholder="e.g., UK" />
placeholder="e.g., UK" maxlength="12" data-maxlength="12" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Flag / Illustration Image</label>
<small class="text-muted d-block mb-1">The desktop image frame is approximately 840x830px. Recommended assets are near-square images at least 1000x1000px.</small>
<div class="input-group mb-2">
<input type="text" class="form-control" id="visaCountriesFlag_0"
value="<%= featured.flag || '' %>" placeholder="/assets/img/home-1/feature/shape.png" />
value="<%= featured.flag || '' %>" placeholder="/assets/img/home-1/feature/shape.png"
maxlength="255" data-maxlength="255" />
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="visaCountriesFlag_0" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload
@@ -82,12 +86,12 @@
<div class="col-md-6">
<label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="visaCountriesLink_0" value="<%= featured.link || '' %>"
placeholder="/country-details/uk" />
placeholder="/country-details/uk" maxlength="255" data-maxlength="255" />
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Visa Types (comma-separated)</label>
<textarea class="form-control" id="visaCountriesVisaTypes_0" rows="2"
placeholder="e.g., Student Visa, Work Visa, Tourist Visa"><%= (featured.visaTypes || []).join(', ') %></textarea>
placeholder="e.g., Student Visa, Work Visa, Tourist Visa" maxlength="220" data-maxlength="220"><%= (featured.visaTypes || []).join(', ') %></textarea>
</div>
</div>
</div>
@@ -109,12 +113,14 @@
<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" />
value="<%= data.visaCountries?.ctaButton?.label || '' %>" placeholder="e.g., Get Started"
maxlength="32" data-maxlength="32" />
</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" />
value="<%= data.visaCountries?.ctaButton?.href || '' %>" placeholder="/contact"
maxlength="255" data-maxlength="255" />
</div>
</div>
</div>
@@ -154,4 +160,4 @@
}
};
};
</script>
</script>

View File

@@ -23,12 +23,14 @@
<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" />
value="<%= data.visaSolutions?.heading || '' %>" placeholder="e.g., Comprehensive Visa Solutions"
maxlength="64" data-maxlength="64" />
</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" />
value="<%= data.visaSolutions?.subheading || '' %>" placeholder="e.g., Our Expert Services"
maxlength="40" data-maxlength="40" />
</div>
</div>
</div>
@@ -62,22 +64,25 @@
<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" />
value="<%= item.number || '' %>" placeholder="e.g., 01"
maxlength="4" data-maxlength="4" />
</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" />
value="<%= item.title || '' %>" placeholder="e.g., Student Visa Guidance"
maxlength="56" data-maxlength="56" />
</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>
placeholder="Enter description" maxlength="180" data-maxlength="180"><%= 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" />
value="<%= item.link || '' %>" placeholder="/service-details"
maxlength="255" data-maxlength="255" />
</div>
</div>
</div>
@@ -146,4 +151,4 @@
updateLabels();
});
</script>
</script>

View File

@@ -24,23 +24,26 @@
<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" />
placeholder="e.g., Turning Study Abroad Dreams Into Reality"
maxlength="72" data-maxlength="72" />
</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" />
value="<%= data.whyChooseUs?.subheading || '' %>" placeholder="e.g., About Our Consultancy"
maxlength="48" data-maxlength="48" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Highlight Word (Optional)</label>
<input type="text" class="form-control" id="whyChooseUsHighlightWord"
value="<%= data.whyChooseUs?.highlightWord || '' %>" placeholder="e.g., Dreams" />
value="<%= data.whyChooseUs?.highlightWord || '' %>" placeholder="e.g., Dreams"
maxlength="24" data-maxlength="24" />
<small class="text-muted">This word in the heading will be wrapped in a colored span.</small>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="whyChooseUsDescription" rows="3"
placeholder="Enter description"><%= data.whyChooseUs?.description || '' %></textarea>
placeholder="Enter description" maxlength="260" data-maxlength="260"><%= data.whyChooseUs?.description || '' %></textarea>
</div>
</div>
</div>
@@ -59,10 +62,11 @@
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Main Image</label>
<small class="text-muted d-block mb-1">Recommended size: 375x419px</small>
<small class="text-muted d-block mb-1">The desktop frame is approximately 318x347px. Recommended upload size is at least 750x820px, with a portrait ratio near 0.91:1.</small>
<div class="input-group mb-2">
<input type="text" class="form-control" id="whyChooseUsMainImage"
value="<%= data.whyChooseUs?.mainImage || '' %>" placeholder="/assets/img/home-1/about/about-1.jpg" />
value="<%= data.whyChooseUs?.mainImage || '' %>" placeholder="/assets/img/home-1/about/about-1.jpg"
maxlength="255" data-maxlength="255" />
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="whyChooseUsMainImage" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload
@@ -78,11 +82,12 @@
<div class="col-md-6">
<label class="form-label fw-medium">Secondary Image</label>
<small class="text-muted d-block mb-1">Recommended size: 376x394px</small>
<small class="text-muted d-block mb-1">The desktop frame is approximately 363x380px. Recommended upload size is at least 760x800px, with a portrait ratio near 0.95:1.</small>
<div class="input-group mb-2">
<input type="text" class="form-control" id="whyChooseUsSecondaryImage"
value="<%= data.whyChooseUs?.secondaryImage || '' %>"
placeholder="/assets/img/home-1/about/about-02.jpg" />
placeholder="/assets/img/home-1/about/about-02.jpg"
maxlength="255" data-maxlength="255" />
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="whyChooseUsSecondaryImage" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload
@@ -117,9 +122,10 @@
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Icon URL</label>
<small class="text-muted d-block mb-1">Icons display at roughly 24x24px. Recommended assets are SVGs or square images at 48x48px.</small>
<div class="input-group mb-2">
<input type="text" class="form-control" id="whyChooseUsIcon_<%= index %>"
value="<%= item.icon || '' %>" />
value="<%= item.icon || '' %>" maxlength="255" data-maxlength="255" />
<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
@@ -129,12 +135,14 @@
<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" />
value="<%= item.title || '' %>" placeholder="e.g., Global Reach"
maxlength="40" data-maxlength="40" />
</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" />
value="<%= item.description || '' %>" placeholder="e.g., Expanding Opportunities Worldwide"
maxlength="72" data-maxlength="72" />
</div>
</div>
</div>
@@ -157,7 +165,7 @@
<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" />
placeholder="Enter feature" maxlength="96" data-maxlength="96" />
</div>
<% }); %>
</div>
@@ -177,12 +185,14 @@
<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" />
value="<%= data.whyChooseUs?.ctaButton?.label || '' %>" placeholder="e.g., Get Started"
maxlength="32" data-maxlength="32" />
</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" />
value="<%= data.whyChooseUs?.ctaButton?.href || '' %>" placeholder="/about"
maxlength="255" data-maxlength="255" />
</div>
</div>
</div>
@@ -241,4 +251,4 @@
},
};
};
</script>
</script>