forked from UKSOURCE/cms.hailearning.edu.vn
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:
@@ -4,11 +4,11 @@
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
|
||||
<%= title %>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Edit content displayed on About Us page</p>
|
||||
<p class="text-muted mb-0">Edit content displayed on the About page</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="<%= frontendUrl %>/about-us/" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-2"></i>View About Us Page
|
||||
<a href="<%= frontendUrl %>/about" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-2"></i>View About Page
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,6 +84,7 @@
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-1">Recommended: 1920x640px wide banner (3:1). Keep the focal subject centered because the image is cropped responsively.</small>
|
||||
<% if (data.hero?.backgroundImage) { %>
|
||||
<img src="<%= data.hero.backgroundImage %>"
|
||||
class="img-thumbnail uploaded-preview mt-2"
|
||||
@@ -129,6 +130,7 @@
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-1">Recommended: 1600x900px landscape image (16:9). This section uses a full-width cover frame.</small>
|
||||
<% if (data.intro?.image) { %>
|
||||
<img src="<%= data.intro.image %>"
|
||||
class="img-thumbnail uploaded-preview mt-2"
|
||||
@@ -175,15 +177,20 @@
|
||||
|
||||
<h6 class="mt-4 mb-3">Images</h6>
|
||||
<div class="row g-3">
|
||||
<% ['main', 'secondary', 'bgShape', 'planeShape', 'topShape', 'globeShape'].forEach(imgKey => { %>
|
||||
<% ['main', 'secondary'].forEach(imgKey => { %>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label"><%= imgKey.charAt(0).toUpperCase() + imgKey.slice(1) %></label>
|
||||
<label class="form-label"><%= imgKey.charAt(0).toUpperCase() + imgKey.slice(1) %> Image</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="missionImg_<%= imgKey %>" value="<%= data.mission?.images?.[imgKey] || '' %>">
|
||||
<button class="btn btn-outline-primary btn-upload-image btn-sm" type="button" data-target-input="missionImg_<%= imgKey %>" data-image-type="about">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
<% if (imgKey === 'main') { %>
|
||||
<small class="text-muted d-block mt-1">Recommended: 1200x1500px portrait image (4:5). This is the primary mission visual.</small>
|
||||
<% } else { %>
|
||||
<small class="text-muted d-block mt-1">Recommended: 1200x900px image (4:3). This image sits as the smaller overlapping card.</small>
|
||||
<% } %>
|
||||
</div>
|
||||
<% }) %>
|
||||
</div>
|
||||
@@ -191,11 +198,12 @@
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label mb-0">Items (Icons & Labels)</label>
|
||||
<label class="form-label mb-0">Items</label>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addMissionItem()">
|
||||
<i class="fas fa-plus me-1"></i>Add
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted d-block mb-2">Mission item icons are fixed by the frontend design and are not editable in CMS.</small>
|
||||
<div id="missionItemsContainer"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@@ -237,6 +245,7 @@
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-1">Recommended: 1920x1080px background image (16:9). Darker imagery works best because the section adds a dark overlay.</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Side Image</label>
|
||||
@@ -246,6 +255,7 @@
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-1">Recommended: 1200x1500px portrait image (4:5). The image fills a tall cover frame on desktop.</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">CTA Button Label</label>
|
||||
@@ -264,6 +274,7 @@
|
||||
<i class="fas fa-plus me-1"></i>Add Item
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted d-block mb-2">Custom icons render at about 28x28px inside a 60x60 card. Use SVG or a square PNG/WebP at 64x64px or 128x128px.</small>
|
||||
<div id="featureItemsContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -312,7 +323,7 @@
|
||||
<input class="form-check-input about-blog-checkbox" type="checkbox" value="<%= blog._id %>" <%= isSelected ? 'checked' : '' %> onclick="event.stopPropagation(); handleAboutCheckboxChange(this)">
|
||||
</div>
|
||||
</div>
|
||||
<img src="<%= blog.featuredImage ? (blog.featuredImage.startsWith('http') ? blog.featuredImage : backendUrl + blog.featuredImage) : '/assets/img/placeholder.jpg' %>" class="card-img-top" style="height: 210px; object-fit: cover;">
|
||||
<img src="<%= blog.featuredImage ? (blog.featuredImage.startsWith('http') ? blog.featuredImage : (blog.featuredImage.startsWith('/') ? backendUrl + blog.featuredImage : backendUrl + '/' + blog.featuredImage)) : '/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 %>
|
||||
@@ -408,7 +419,7 @@
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
showToast('Success', 'About Us updated successfully', 'success');
|
||||
showToast('Success', 'About page updated successfully', 'success');
|
||||
|
||||
// Update the local state with returned data from server
|
||||
// This ensures the UI is in sync with what was actually saved
|
||||
@@ -640,7 +651,7 @@
|
||||
document.getElementById('missionCtaLabel').value = mission.ctaButton?.label || '';
|
||||
document.getElementById('missionCtaHref').value = mission.ctaButton?.href || '';
|
||||
|
||||
['main', 'secondary', 'bgShape', 'planeShape', 'topShape', 'globeShape'].forEach(k => {
|
||||
['main', 'secondary'].forEach(k => {
|
||||
const el = document.getElementById('missionImg_' + k);
|
||||
const val = mission.images?.[k] || '';
|
||||
if (el) {
|
||||
@@ -711,18 +722,10 @@
|
||||
<div class="card mb-2 mission-item">
|
||||
<div class="card-body p-2">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" name="missionItemIcon_${idx}" placeholder="Icon path">
|
||||
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="missionItemIcon_${idx}" data-image-type="about">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control form-control-sm" name="missionItemLabel_${idx}" placeholder="Label">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-7">
|
||||
<input type="text" class="form-control form-control-sm" name="missionItemDesc_${idx}" placeholder="Description">
|
||||
</div>
|
||||
</div>
|
||||
@@ -735,12 +738,11 @@
|
||||
function populateMissionItems(items) {
|
||||
const container = document.getElementById('missionItemsContainer');
|
||||
container.innerHTML = '';
|
||||
items.forEach((item, i) => {
|
||||
items.forEach((item) => {
|
||||
addMissionItem();
|
||||
const last = container.lastElementChild;
|
||||
last.querySelector(`[name="missionItemIcon_${i}"]`).value = item.icon || '';
|
||||
last.querySelector(`[name="missionItemLabel_${i}"]`).value = item.label || '';
|
||||
last.querySelector(`[name="missionItemDesc_${i}"]`).value = item.description || '';
|
||||
last.querySelector(`[name^="missionItemLabel_"]`).value = item.label || item.title || '';
|
||||
last.querySelector(`[name^="missionItemDesc_"]`).value = item.description || '';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -895,14 +897,10 @@
|
||||
description: document.getElementById('missionDescription').value.trim(),
|
||||
images: {
|
||||
main: document.getElementById('missionImg_main').value.trim(),
|
||||
secondary: document.getElementById('missionImg_secondary').value.trim(),
|
||||
bgShape: document.getElementById('missionImg_bgShape').value.trim(),
|
||||
planeShape: document.getElementById('missionImg_planeShape').value.trim(),
|
||||
topShape: document.getElementById('missionImg_topShape').value.trim(),
|
||||
globeShape: document.getElementById('missionImg_globeShape').value.trim()
|
||||
secondary: document.getElementById('missionImg_secondary').value.trim()
|
||||
},
|
||||
items: Array.from(document.querySelectorAll('.mission-item')).map(item => ({
|
||||
icon: item.querySelector('[name^="missionItemIcon_"]').value.trim(),
|
||||
icon: '/assets/img/home-1/icon/01.svg',
|
||||
label: item.querySelector('[name^="missionItemLabel_"]').value.trim(),
|
||||
description: item.querySelector('[name^="missionItemDesc_"]').value.trim()
|
||||
})).filter(i => i.label !== ''),
|
||||
@@ -969,4 +967,4 @@
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -41,7 +41,9 @@
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="heroBannerImage" id="heroBannerImage"
|
||||
value="<%= (data.hero && data.hero.bannerImage) || '' %>"
|
||||
placeholder="/templates/yootheme/activities/activity-banner.jpg">
|
||||
placeholder="/templates/yootheme/activities/activity-banner.jpg"
|
||||
maxlength="255" data-maxlength="255"
|
||||
data-admin-upload-guidance="Activity page hero-style image.|Recommended upload: at least 1920x700px.">
|
||||
<button type="button" class="btn btn-outline-secondary" id="uploadHeroBannerBtn">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
@@ -235,7 +237,9 @@
|
||||
<label class="form-label fw-medium">Image Path</label>
|
||||
<input type="text" class="form-control" name="image" id="imageInput"
|
||||
value="<%= data.image || '' %>"
|
||||
placeholder="e.g., yootheme/banner/b14.jpg">
|
||||
placeholder="e.g., yootheme/banner/b14.jpg"
|
||||
maxlength="255" data-maxlength="255"
|
||||
data-admin-upload-guidance="Used in activity listing cards.|Recommended upload: a landscape image at 704x432px or larger.">
|
||||
<small class="text-muted">Path to the main activity image (used in listings)</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -271,7 +275,9 @@
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="campDetailHeroBgImage" id="campDetailHeroBgImage"
|
||||
value="<%= (data.campDetail && data.campDetail.hero && data.campDetail.hero.bgImage) || '' %>"
|
||||
placeholder="e.g., yootheme/banner/b1.jpg">
|
||||
placeholder="e.g., yootheme/banner/b1.jpg"
|
||||
maxlength="255" data-maxlength="255"
|
||||
data-admin-upload-guidance="Activity page hero-style image.|Recommended upload: at least 1920x700px.">
|
||||
<button type="button" class="btn btn-outline-secondary" id="uploadHeroBgBtn"><i class="fas fa-upload me-1"></i>Upload</button>
|
||||
</div>
|
||||
<div class="mt-2" id="campDetailHeroBgPreviewWrapper" style="display: none;">
|
||||
@@ -4146,4 +4152,4 @@
|
||||
const bsModal = new bootstrap.Modal(modal);
|
||||
bsModal.show();
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -767,16 +767,16 @@
|
||||
formData.append('image', file);
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/upload/image', {
|
||||
const response = await fetch(`/admin/upload/image?imageType=${encodeURIComponent(imageType || 'appointment')}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success && result.imagePath) {
|
||||
document.getElementById(targetInput).value = result.imagePath;
|
||||
if (result.success && result.path) {
|
||||
document.getElementById(targetInput).value = result.path;
|
||||
if (targetInput === 'heroBackgroundImage') {
|
||||
updateHeroImagePreview(result.imagePath);
|
||||
updateHeroImagePreview(result.path);
|
||||
}
|
||||
} else {
|
||||
alert('Upload failed: ' + (result.error || 'Unknown error'));
|
||||
@@ -788,4 +788,4 @@
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -165,9 +165,7 @@
|
||||
<textarea class="form-control" id="excerpt" name="excerpt" rows="3"
|
||||
required maxlength="500"
|
||||
placeholder="Enter a brief summary of the blog post (max 500 characters)"></textarea>
|
||||
<div class="form-text">
|
||||
<span id="excerptCount">0</span>/500 characters
|
||||
</div>
|
||||
<div class="form-text">Maximum 500 characters.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -470,14 +468,6 @@
|
||||
console.error('Error initializing contentAfterQuote editor:', error);
|
||||
}
|
||||
|
||||
// Excerpt character counter
|
||||
const excerptInput = document.getElementById('excerpt');
|
||||
const excerptCount = document.getElementById('excerptCount');
|
||||
|
||||
excerptInput.addEventListener('input', function () {
|
||||
excerptCount.textContent = this.value.length;
|
||||
});
|
||||
|
||||
// Image upload handler
|
||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
@@ -1062,4 +1052,4 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -200,11 +200,7 @@
|
||||
<textarea class="form-control" id="excerpt" name="excerpt" rows="3"
|
||||
required maxlength="500"
|
||||
placeholder="Enter a brief summary of the blog post (max 500 characters)"><%= blog.excerpt || '' %></textarea>
|
||||
<div class="form-text">
|
||||
<span id="excerptCount">
|
||||
<%= (blog.excerpt || '' ).length %>
|
||||
</span>/500 characters
|
||||
</div>
|
||||
<div class="form-text">Maximum 500 characters.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -888,14 +884,6 @@
|
||||
console.error('Error initializing contentAfterQuote editor:', error);
|
||||
}
|
||||
|
||||
// Excerpt character counter
|
||||
const excerptInput = document.getElementById('excerpt');
|
||||
const excerptCount = document.getElementById('excerptCount');
|
||||
|
||||
excerptInput.addEventListener('input', function () {
|
||||
excerptCount.textContent = this.value.length;
|
||||
});
|
||||
|
||||
// Image upload handler
|
||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
@@ -1773,4 +1761,4 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -2000,13 +2000,13 @@
|
||||
}
|
||||
|
||||
|
||||
if (window.toastManager) window.toastManager.success('Upload thành công');
|
||||
if (window.toastManager) window.toastManager.success('Upload completed successfully');
|
||||
} else {
|
||||
if (window.toastManager) window.toastManager.success('Upload thành công');
|
||||
if (window.toastManager) window.toastManager.success('Upload completed successfully');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Upload error', err);
|
||||
if (window.toastManager) window.toastManager.error('Lỗi upload: ' + (err.message ||
|
||||
if (window.toastManager) window.toastManager.error('Upload error: ' + (err.message ||
|
||||
err));
|
||||
}
|
||||
});
|
||||
@@ -2015,7 +2015,7 @@
|
||||
fileInput.click();
|
||||
} catch (err) {
|
||||
console.error('openImageUploader error', err);
|
||||
if (window.toastManager) window.toastManager.error('Lỗi: ' + (err.message || err));
|
||||
if (window.toastManager) window.toastManager.error('Error: ' + (err.message || err));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2031,4 +2031,4 @@
|
||||
}
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<p class="text-muted mb-0">Edit content displayed on Contact Us page</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="<%= frontendUrl %>/contact-us/" class="btn btn-outline-primary" target="_blank">
|
||||
<a href="<%= frontendUrl %>/contact" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-2"></i>View Contact Us Page
|
||||
</a>
|
||||
</div>
|
||||
@@ -66,7 +66,8 @@
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="heroBackgroundImage"
|
||||
name="heroBackgroundImage"
|
||||
value="<%= data.hero?.backgroundImage || '' %>">
|
||||
value="<%= data.hero?.backgroundImage || '' %>"
|
||||
maxlength="255" data-maxlength="255">
|
||||
<button type="button"
|
||||
class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="heroBackgroundImage"
|
||||
@@ -74,7 +75,7 @@
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted">Recommended size: 1920x1080px</small>
|
||||
<small class="text-muted d-block mt-1">Recommended minimum upload: 1920x700px.</small>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div id="heroImagePreview" style="height: 300px;">
|
||||
@@ -106,7 +107,9 @@
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="heroTitle" name="heroTitle"
|
||||
value="<%= data.hero?.title || '' %>">
|
||||
value="<%= data.hero?.title || '' %>"
|
||||
maxlength="40" data-maxlength="40">
|
||||
<small class="text-muted d-block mt-1">Keep the hero title short so the centered breadcrumb stays balanced on tablet and mobile.</small>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Hidden field for overlayColor - keep default value -->
|
||||
@@ -176,7 +179,8 @@
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control"
|
||||
name="cardTitle_<%= index %>"
|
||||
value="<%= card.title || '' %>">
|
||||
value="<%= card.title || '' %>"
|
||||
maxlength="40" data-maxlength="40">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Icon Type</label>
|
||||
@@ -324,7 +328,8 @@
|
||||
class="form-control card-icon-image-input"
|
||||
name="cardIconImage_<%= index %>"
|
||||
value="<%= imageIconValue %>"
|
||||
placeholder="/uploads/icon.png">
|
||||
placeholder="/uploads/icon.png"
|
||||
maxlength="255" data-maxlength="255">
|
||||
<button type="button"
|
||||
class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="cardIconImage_<%= index %>"
|
||||
@@ -347,9 +352,7 @@
|
||||
style="max-height: 100px; width: auto; display: none;"
|
||||
alt="Icon preview">
|
||||
<% } %>
|
||||
<small class="text-muted">Upload
|
||||
a custom icon image for this
|
||||
contact card</small>
|
||||
<small class="text-muted d-block mt-1">Custom icons render at about 28x28px inside a 64x64 circle. Use SVG or a square PNG/WebP at 64x64px or 128x128px.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
@@ -357,9 +360,8 @@
|
||||
line)</label>
|
||||
<textarea class="form-control"
|
||||
name="cardContent_<%= index %>"
|
||||
rows="3"><%= (card.content || []).join('\n') %></textarea>
|
||||
<small class="text-muted">Enter each content
|
||||
item on a new line</small>
|
||||
rows="3" maxlength="220" data-maxlength="220"><%= (card.content || []).join('\n') %></textarea>
|
||||
<small class="text-muted d-block mt-1">Each line is shown inside a compact contact card. Keep it to 1-3 short lines.</small>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
@@ -386,23 +388,24 @@
|
||||
<label class="form-label">Marker Title</label>
|
||||
<input type="text" class="form-control" id="mapMarkerTitle"
|
||||
value="<%= data.map?.markerTitle || '' %>"
|
||||
placeholder="e.g., Our Office">
|
||||
placeholder="e.g., Our Office"
|
||||
maxlength="48" data-maxlength="48">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Location</label>
|
||||
<input type="text" class="form-control" id="mapLocation"
|
||||
value="<%= data.map?.location || '' %>"
|
||||
placeholder="e.g., 123 Main St, City, Country">
|
||||
<small class="text-muted">Enter address - map will be automatically
|
||||
shown</small>
|
||||
placeholder="e.g., 123 Main St, City, Country"
|
||||
maxlength="120" data-maxlength="120">
|
||||
<small class="text-muted d-block mt-1">Enter a full address. This text is used for map lookup and should stay concise.</small>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Google Map Embed URL</label>
|
||||
<input type="text" class="form-control" id="mapEmbedUrl"
|
||||
value="<%= data.map?.embedUrl || '' %>"
|
||||
placeholder="https://www.google.com/maps/embed?...">
|
||||
<small class="text-muted">Paste embed URL from Google Maps (Share ->
|
||||
Embed a map)</small>
|
||||
placeholder="https://www.google.com/maps/embed?..."
|
||||
maxlength="1000" data-maxlength="1000">
|
||||
<small class="text-muted d-block mt-1">Paste the Google Maps embed URL from Share -> Embed a map.</small>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<div id="mapPreview"
|
||||
@@ -468,22 +471,29 @@
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Section Label</label>
|
||||
<input type="text" class="form-control" id="formSectionLabel"
|
||||
value="<%= data.form?.sectionLabel || '' %>">
|
||||
value="<%= data.form?.sectionLabel || '' %>"
|
||||
maxlength="32" data-maxlength="32">
|
||||
<small class="text-muted d-block mt-1">Legacy label. Keep it short if you still use it in future templates.</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Submit Button Text</label>
|
||||
<input type="text" class="form-control" id="formSubmitButtonText"
|
||||
value="<%= data.form?.submitButton?.text || 'Send Message' %>">
|
||||
value="<%= data.form?.submitButton?.text || 'Send Message' %>"
|
||||
maxlength="24" data-maxlength="24">
|
||||
<small class="text-muted d-block mt-1">Recommended maximum: 24 characters.</small>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Heading</label>
|
||||
<input type="text" class="form-control" id="formHeading"
|
||||
value="<%= data.form?.heading || '' %>">
|
||||
value="<%= data.form?.heading || '' %>"
|
||||
maxlength="48" data-maxlength="48">
|
||||
<small class="text-muted d-block mt-1">The form heading spans the full form width. Recommended maximum: 48 characters.</small>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="formDescription"
|
||||
rows="2"><%= data.form?.description || '' %></textarea>
|
||||
rows="2" maxlength="160" data-maxlength="160"><%= data.form?.description || '' %></textarea>
|
||||
<small class="text-muted d-block mt-1">This line is centered under the form heading. Recommended maximum: 160 characters.</small>
|
||||
</div>
|
||||
<!-- Hidden fields for submitButton icon and buttonClass -->
|
||||
<input type="hidden" id="formSubmitButtonIcon"
|
||||
@@ -510,7 +520,9 @@
|
||||
<input type="text" class="form-control"
|
||||
name="fieldName_<%= index %>"
|
||||
value="<%= field.name || '' %>"
|
||||
placeholder="e.g., Your Name">
|
||||
placeholder="e.g., Your Name"
|
||||
maxlength="32" data-maxlength="32">
|
||||
<small class="text-muted d-block mt-1">Keep field labels short for the stacked mobile form.</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Field Type</label>
|
||||
@@ -536,7 +548,8 @@
|
||||
<label class="form-label">Placeholder</label>
|
||||
<input type="text" class="form-control"
|
||||
name="fieldPlaceholder_<%= index %>"
|
||||
value="<%= field.placeholder || '' %>">
|
||||
value="<%= field.placeholder || '' %>"
|
||||
maxlength="72" data-maxlength="72">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Required</label>
|
||||
@@ -552,9 +565,9 @@
|
||||
<input type="text" class="form-control"
|
||||
name="fieldProgrammeName_<%= index %>"
|
||||
value="<%= field.programmeName || '' %>"
|
||||
placeholder="e.g., Summer Camp 2024">
|
||||
<small class="text-muted">Internal name for the
|
||||
programme</small>
|
||||
placeholder="e.g., Summer Camp 2024"
|
||||
maxlength="48" data-maxlength="48">
|
||||
<small class="text-muted d-block mt-1">Internal programme reference only. Recommended maximum: 48 characters.</small>
|
||||
</div>
|
||||
<!-- Hidden fields for label and colClass -->
|
||||
<input type="hidden" name="fieldLabel_<%= index %>"
|
||||
@@ -775,6 +788,7 @@
|
||||
|
||||
updateAllJsonInputs(originalFormData);
|
||||
initializeFormHandlers();
|
||||
initContactCharacterCounters(document);
|
||||
});
|
||||
|
||||
function applyDateFilter() {
|
||||
@@ -1195,6 +1209,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
function initContactCharacterCounters(scope = document) {
|
||||
scope.querySelectorAll('.contact-limit-counter').forEach((node) => node.remove());
|
||||
|
||||
if (window.AdminFormHelpers) {
|
||||
window.AdminFormHelpers.refresh(scope);
|
||||
}
|
||||
}
|
||||
|
||||
function updateAllJsonInputs(data) {
|
||||
document.getElementById('heroJson').value = JSON.stringify(data.hero || {});
|
||||
document.getElementById('contactCardsJson').value = JSON.stringify(data.contactCards || []);
|
||||
@@ -1252,7 +1274,7 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" name="cardTitle_${index}">
|
||||
<input type="text" class="form-control" name="cardTitle_${index}" maxlength="40" data-maxlength="40">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Icon Type</label>
|
||||
@@ -1306,20 +1328,20 @@
|
||||
<label class="form-label">Upload Icon Image</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control card-icon-image-input" name="cardIconImage_${index}"
|
||||
placeholder="/uploads/icon.png">
|
||||
placeholder="/uploads/icon.png" maxlength="255" data-maxlength="255">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="cardIconImage_${index}" data-image-type="contact">
|
||||
<i class="fas fa-upload me-1"></i>Upload Icon
|
||||
</button>
|
||||
</div>
|
||||
<img src="" class="img-thumbnail mt-2 icon-image-preview" data-index="${index}" style="max-height: 100px; width: auto; display: none;" alt="Icon preview">
|
||||
<small class="text-muted">Upload a custom icon image for this contact card</small>
|
||||
<small class="text-muted d-block mt-1">Custom icons render at about 28x28px inside a 64x64 circle. Use SVG or a square PNG/WebP at 64x64px or 128x128px.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Content (one per line)</label>
|
||||
<textarea class="form-control" name="cardContent_${index}" rows="3"></textarea>
|
||||
<small class="text-muted">Enter each content item on a new line</small>
|
||||
<textarea class="form-control" name="cardContent_${index}" rows="3" maxlength="220" data-maxlength="220"></textarea>
|
||||
<small class="text-muted d-block mt-1">Each line is shown inside a compact contact card. Keep it to 1-3 short lines.</small>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeContactCard(this)">
|
||||
@@ -1340,6 +1362,8 @@
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
}
|
||||
|
||||
initContactCharacterCounters(newCard);
|
||||
}
|
||||
|
||||
function handleIconSourceChange(radio) {
|
||||
@@ -1385,7 +1409,8 @@
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Label</label>
|
||||
<input type="text" class="form-control" name="fieldName_${index}" placeholder="e.g., Your Name">
|
||||
<input type="text" class="form-control" name="fieldName_${index}" placeholder="e.g., Your Name" maxlength="32" data-maxlength="32">
|
||||
<small class="text-muted d-block mt-1">Keep field labels short for the stacked mobile form.</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Field Type</label>
|
||||
@@ -1399,7 +1424,7 @@
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Placeholder</label>
|
||||
<input type="text" class="form-control" name="fieldPlaceholder_${index}">
|
||||
<input type="text" class="form-control" name="fieldPlaceholder_${index}" maxlength="72" data-maxlength="72">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Required</label>
|
||||
@@ -1409,8 +1434,8 @@
|
||||
</div>
|
||||
<div class="col-md-3 programme-name-field" style="display: none;">
|
||||
<label class="form-label">Programme Name</label>
|
||||
<input type="text" class="form-control" name="fieldProgrammeName_${index}" placeholder="e.g., Summer Camp 2024">
|
||||
<small class="text-muted">Internal name for the programme</small>
|
||||
<input type="text" class="form-control" name="fieldProgrammeName_${index}" placeholder="e.g., Summer Camp 2024" maxlength="48" data-maxlength="48">
|
||||
<small class="text-muted d-block mt-1">Internal programme reference only. Recommended maximum: 48 characters.</small>
|
||||
</div>
|
||||
<!-- Hidden fields for label and colClass -->
|
||||
<input type="hidden" name="fieldLabel_${index}" value="">
|
||||
@@ -1423,6 +1448,7 @@
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
initContactCharacterCounters(container.lastElementChild);
|
||||
}
|
||||
|
||||
function removeFormField(button) {
|
||||
@@ -1662,4 +1688,4 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -634,8 +634,8 @@
|
||||
|
||||
async function loadFooterData() {
|
||||
try {
|
||||
console.log("Fetching footer data from /api/footer...");
|
||||
const response = await fetch("/api/footer");
|
||||
console.log("Fetching footer data from /admin/footer/data...");
|
||||
const response = await fetch("/admin/footer/data");
|
||||
console.log("Response status:", response.status);
|
||||
console.log("Response ok:", response.ok);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<div class="content-with-fixed-buttons">
|
||||
<!-- Hidden inputs for JSON data -->
|
||||
<input type="hidden" name="topbarJson" id="topbarJson" />
|
||||
<input type="hidden" name="offcanvasJson" id="offcanvasJson" />
|
||||
<input type="hidden" name="logo" id="logoInput" />
|
||||
<input type="hidden" name="activeTab" id="activeTabInput" value="topbar" />
|
||||
<input type="hidden" name="menuUpdates" id="menuUpdates" />
|
||||
@@ -99,6 +100,25 @@
|
||||
placeholder="69 Street, 5th Avenue LA, United States"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Working Hours <small class="text-muted fw-normal">(Drawer Menu)</small></label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="offcanvasWorkingHours"
|
||||
value="<%= (data.header && data.header.offcanvas && data.header.offcanvas.contactInfo && data.header.offcanvas.contactInfo.workingHours) ? data.header.offcanvas.contactInfo.workingHours : '' %>"
|
||||
placeholder="Mon-Friday, 09am - 05pm"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Offcanvas Description <small class="text-muted fw-normal">(Drawer Menu)</small></label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="offcanvasDescription"
|
||||
rows="3"
|
||||
placeholder="Short description displayed in the offcanvas sidebar..."
|
||||
><%= (data.header && data.header.offcanvas && data.header.offcanvas.description) ? data.header.offcanvas.description : '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,6 +199,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Menu Structure Tab -->
|
||||
<div class="tab-pane fade <%= activeTab === 'menu' ? 'show active' : '' %>" id="menu" role="tabpanel">
|
||||
<%- include('menu') %>
|
||||
@@ -349,6 +370,7 @@
|
||||
// 1. Collect and Save Topbar & Logo
|
||||
const headerData = {
|
||||
topbarJson: document.getElementById('topbarJson').value,
|
||||
offcanvasJson: document.getElementById('offcanvasJson').value,
|
||||
logo: document.getElementById('logoInput').value,
|
||||
activeTab: document.getElementById('activeTabInput').value
|
||||
};
|
||||
@@ -382,7 +404,7 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('=== TRACE: Unified Save ERROR ===', error);
|
||||
showNotification('Lỗi: ' + error.message, 'error');
|
||||
showNotification('Error: ' + error.message, 'error');
|
||||
} finally {
|
||||
submitBtn.innerHTML = originalText;
|
||||
submitBtn.disabled = false;
|
||||
@@ -421,6 +443,18 @@
|
||||
document.getElementById('topbarJson').value = JSON.stringify(topbarData);
|
||||
document.getElementById('logoInput').value = document.getElementById('logoImage').value || '';
|
||||
|
||||
// Collect offcanvas data — phone/email/address shared from topbar
|
||||
const offcanvasData = {
|
||||
description: document.getElementById('offcanvasDescription').value || '',
|
||||
contactInfo: {
|
||||
phone: document.getElementById('contactPhone').value || '',
|
||||
email: document.getElementById('contactEmail').value || '',
|
||||
address: document.getElementById('contactLocation').value || '',
|
||||
workingHours: document.getElementById('offcanvasWorkingHours').value || ''
|
||||
}
|
||||
};
|
||||
document.getElementById('offcanvasJson').value = JSON.stringify(offcanvasData);
|
||||
|
||||
try {
|
||||
const menuUpdates = collectMenuUpdates();
|
||||
document.getElementById('menuUpdates').value = JSON.stringify(menuUpdates);
|
||||
@@ -675,19 +709,19 @@
|
||||
const icon = document.getElementById('newSocialIcon').value.trim();
|
||||
|
||||
if (!platform) {
|
||||
alert('Vui lòng nhập tên nền tảng');
|
||||
alert('Please enter a platform name');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
alert('Vui lòng nhập URL');
|
||||
alert('Please enter a URL');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if platform already exists
|
||||
const existingPlatforms = Array.from(document.querySelectorAll('.social-platform')).map(el => el.value);
|
||||
if (existingPlatforms.includes(platform)) {
|
||||
alert(`${platform} đã tồn tại`);
|
||||
alert(`${platform} already exists`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -746,12 +780,12 @@
|
||||
const newIcon = document.getElementById('editSocialIcon').value.trim();
|
||||
|
||||
if (!newPlatform) {
|
||||
alert('Vui lòng nhập tên nền tảng');
|
||||
alert('Please enter a platform name');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newUrl) {
|
||||
alert('Vui lòng nhập URL');
|
||||
alert('Please enter a URL');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1067,6 +1101,17 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium d-block">Maintenance Mode</label>
|
||||
<input type="hidden" name="is_maintainance" value="false">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" name="is_maintainance" id="formMaintainance" value="true">
|
||||
<label class="form-check-label" for="formMaintainance">
|
||||
Redirect this menu page to the maintenance screen while it is being repaired.
|
||||
</label>
|
||||
</div>
|
||||
<small class="text-muted">Use this when the linked page should be temporarily unavailable to visitors.</small>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label class="form-label fw-medium">Link Type</label>
|
||||
<div class="d-flex gap-3">
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
<% } else { %>
|
||||
<span class="badge bg-soft-success ms-2">Active</span>
|
||||
<% } %>
|
||||
<% if (item.is_maintainance) { %>
|
||||
<span class="badge ms-2 bg-warning text-dark">Maintenance</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="text-muted small text-truncate" style="max-width: 300px;">
|
||||
<i class="fas fa-link me-1" style="font-size: 0.75rem;"></i><%= item.url %>
|
||||
@@ -192,6 +195,7 @@
|
||||
document.getElementById('formUrl').value = '';
|
||||
document.getElementById('formOrder').value = '0';
|
||||
document.getElementById('formStatus').value = 'active';
|
||||
document.getElementById('formMaintainance').checked = false;
|
||||
document.getElementById('typeInternal').checked = true;
|
||||
|
||||
const modalElement = document.getElementById('modalAddMenu');
|
||||
@@ -216,6 +220,7 @@
|
||||
document.getElementById('formUrl').value = item.url;
|
||||
document.getElementById('formOrder').value = item.order;
|
||||
document.getElementById('formStatus').value = item.status;
|
||||
document.getElementById('formMaintainance').checked = Boolean(item.is_maintainance);
|
||||
|
||||
if (item.type === 'external') {
|
||||
document.getElementById('typeExternal').checked = true;
|
||||
|
||||
@@ -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(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/&/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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
483
views/admin/home/sections/floatingContact.ejs
Normal file
483
views/admin/home/sections/floatingContact.ejs
Normal 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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -529,7 +529,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
img.alt = 'Image preview';
|
||||
|
||||
if (window.toastManager) {
|
||||
window.toastManager.success('Tải ảnh thành công');
|
||||
window.toastManager.success('Image uploaded successfully');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Country Icon (Flag)</label>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" name="icon" id="icon_input" class="form-control" placeholder="/uploads/visa/flag.png" required />
|
||||
<input type="text" name="icon" id="icon_input" class="form-control" placeholder="/uploads/visa/flag.png" maxlength="255" data-maxlength="255" data-admin-upload-guidance="Displayed as a small country flag or icon.|Prefer SVG; otherwise use a square image at 96x96px or larger." required />
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="document.getElementById('fileFlagInput').click()">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
@@ -146,7 +146,7 @@
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Main Image</label>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" id="mainImage_detail" name="mainImage" class="form-control" placeholder="/uploads/visa/details-1.jpg" required />
|
||||
<input type="text" id="mainImage_detail" name="mainImage" class="form-control" placeholder="/uploads/visa/details-1.jpg" maxlength="255" data-maxlength="255" data-admin-upload-guidance="Used in visa detail content blocks.|Recommended upload: at least 1000x750px for primary imagery." required />
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="document.getElementById('fileDetailMainInput').click()">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
@@ -236,7 +236,7 @@
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Gallery Image <%= i %></label>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="bannerImageGallery<%= i %>" name="bannerImageGallery" placeholder="https://example.com/image.jpg" required />
|
||||
<input type="text" class="form-control" id="bannerImageGallery<%= i %>" name="bannerImageGallery" placeholder="https://example.com/image.jpg" maxlength="255" data-maxlength="255" data-admin-upload-guidance="Gallery image used in visa detail content.|Recommended upload: at least 1000x750px." required />
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="document.getElementById('fileGalleryInput<%= i %>').click()">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
@@ -330,7 +330,7 @@
|
||||
<div class="col-md-5">
|
||||
<label class="form-label fw-medium small">Icon</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" name="related_icon[]" id="related_url_<%= i %>" class="form-control form-control-sm" placeholder="/uploads/visa/icon.png" required />
|
||||
<input type="text" name="related_icon[]" id="related_url_<%= i %>" class="form-control form-control-sm" placeholder="/uploads/visa/icon.png" maxlength="255" data-maxlength="255" data-admin-upload-guidance="Supporting service image.|Recommended upload: at least 800x600px." required />
|
||||
<input type="file" id="related_file_<%= i %>" class="d-none" accept="image/*">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="document.getElementById('related_file_<%= i %>').click()">
|
||||
<i class="fas fa-upload"></i>
|
||||
@@ -365,7 +365,7 @@
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Image Contact</label>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" id="contact_image_input" name="contact_image" class="form-control" placeholder="/uploads/visa/contact-image.jpg" required />
|
||||
<input type="text" id="contact_image_input" name="contact_image" class="form-control" placeholder="/uploads/visa/contact-image.jpg" maxlength="255" data-maxlength="255" data-admin-upload-guidance="Contact-side supporting image.|Recommended upload: at least 800x600px." required />
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="document.getElementById('fileContactImageInput').click()">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
@@ -599,17 +599,75 @@
|
||||
setupImageUploadHandlers();
|
||||
});
|
||||
|
||||
function showListView() {
|
||||
document.getElementById("tableContainer").style.display = "block";
|
||||
document.getElementById("formContainer").style.display = "none";
|
||||
document.getElementById("viewContainer").style.display = "none";
|
||||
}
|
||||
|
||||
function showFormView() {
|
||||
document.getElementById("tableContainer").style.display = "none";
|
||||
document.getElementById("formContainer").style.display = "block";
|
||||
document.getElementById("viewContainer").style.display = "none";
|
||||
}
|
||||
function showListView() {
|
||||
document.getElementById("tableContainer").style.display = "block";
|
||||
document.getElementById("formContainer").style.display = "none";
|
||||
document.getElementById("viewContainer").style.display = "none";
|
||||
}
|
||||
|
||||
function refreshVisaAdminHelpers() {
|
||||
if (!window.AdminFormHelpers) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formContainer = document.getElementById("formContainer");
|
||||
window.AdminFormHelpers.refresh(formContainer);
|
||||
|
||||
const guidanceTargets = [
|
||||
{
|
||||
selector: "#icon_input",
|
||||
lines: [
|
||||
"Displayed as a small country flag or icon.",
|
||||
"Prefer SVG; otherwise use a square image at 96x96px or larger."
|
||||
]
|
||||
},
|
||||
{
|
||||
selector: "#mainImage_detail",
|
||||
lines: [
|
||||
"Used in visa detail content blocks.",
|
||||
"Recommended upload: at least 1000x750px for primary imagery."
|
||||
]
|
||||
},
|
||||
{
|
||||
selector: "input[name='bannerImageGallery']",
|
||||
lines: [
|
||||
"Gallery image used in visa detail content.",
|
||||
"Recommended upload: at least 1000x750px."
|
||||
]
|
||||
},
|
||||
{
|
||||
selector: "input[name='related_icon[]']",
|
||||
lines: [
|
||||
"Supporting service image.",
|
||||
"Recommended upload: at least 800x600px."
|
||||
]
|
||||
},
|
||||
{
|
||||
selector: "#contact_image_input",
|
||||
lines: [
|
||||
"Contact-side supporting image.",
|
||||
"Recommended upload: at least 800x600px."
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
guidanceTargets.forEach(({ selector, lines }) => {
|
||||
document.querySelectorAll(selector).forEach((input) => {
|
||||
window.AdminFormHelpers.renderUploadGuidance(input, {
|
||||
for: input.id || input.name || "",
|
||||
title: "Upload guidance",
|
||||
lines
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showFormView() {
|
||||
document.getElementById("tableContainer").style.display = "none";
|
||||
document.getElementById("formContainer").style.display = "block";
|
||||
document.getElementById("viewContainer").style.display = "none";
|
||||
refreshVisaAdminHelpers();
|
||||
}
|
||||
|
||||
function showViewView() {
|
||||
document.getElementById("tableContainer").style.display = "none";
|
||||
@@ -795,12 +853,13 @@
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
showFormView();
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
showNotification('Cannot connect to server. Please try again.', 'error');
|
||||
|
||||
showFormView();
|
||||
refreshVisaAdminHelpers();
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
showNotification('Cannot connect to server. Please try again.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -892,9 +951,10 @@
|
||||
if (field) field.value = "";
|
||||
});
|
||||
|
||||
|
||||
showFormView();
|
||||
});
|
||||
|
||||
showFormView();
|
||||
refreshVisaAdminHelpers();
|
||||
});
|
||||
|
||||
document.getElementById("btnBackToList").addEventListener("click", showListView);
|
||||
document.getElementById("btnBackFromView").addEventListener("click", showListView);
|
||||
@@ -1031,4 +1091,4 @@
|
||||
btnSave.innerHTML = '<i class="fas fa-save me-2"></i>Save';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user