forked from UKSOURCE/cms.hailearning.edu.vn
feat: standardize admin form limits and guidance
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-1">The contact hero currently renders at about 1496x544px on desktop. Recommended minimum upload: 1920x700px.</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;">
|
||||
@@ -181,7 +181,6 @@
|
||||
name="cardTitle_<%= index %>"
|
||||
value="<%= card.title || '' %>"
|
||||
maxlength="40" data-maxlength="40">
|
||||
<small class="text-muted d-block mt-1">Recommended maximum: 40 characters.</small>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Icon Type</label>
|
||||
@@ -1210,63 +1209,12 @@
|
||||
});
|
||||
}
|
||||
|
||||
function ensureContactCounter(input) {
|
||||
if (!input || !input.dataset.maxlength) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!input.id) {
|
||||
input.id = `contactField_${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
|
||||
const field = input.closest('.col-md-12, .col-md-7, .col-md-6, .col-md-5, .col-md-4, .col-md-3, .col-md-2, .col-12') || input.parentElement;
|
||||
const anchor = input.closest('.input-group') || input;
|
||||
const parent = anchor?.parentElement || field;
|
||||
if (!field || !anchor || !parent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let hint = field.querySelector(`[data-counter-for="${input.id}"]`);
|
||||
if (!hint) {
|
||||
hint = document.createElement('small');
|
||||
hint.className = 'form-text contact-limit-counter text-secondary';
|
||||
hint.dataset.counterFor = input.id;
|
||||
}
|
||||
|
||||
if (hint.previousElementSibling !== anchor) {
|
||||
parent.insertBefore(hint, anchor.nextSibling);
|
||||
}
|
||||
|
||||
return hint;
|
||||
}
|
||||
|
||||
function updateContactCounter(input) {
|
||||
const hint = ensureContactCounter(input);
|
||||
if (!hint) {
|
||||
return;
|
||||
}
|
||||
|
||||
const max = Number(input.dataset.maxlength);
|
||||
if (Number.isFinite(max) && max > 0 && (input.value || '').length > max) {
|
||||
input.value = (input.value || '').slice(0, max);
|
||||
}
|
||||
|
||||
const length = (input.value || '').length;
|
||||
hint.textContent = `${length}/${max} characters`;
|
||||
hint.classList.toggle('text-danger', length >= max);
|
||||
}
|
||||
|
||||
function initContactCharacterCounters(scope = document) {
|
||||
scope.querySelectorAll('input[data-maxlength], textarea[data-maxlength]').forEach((input) => {
|
||||
updateContactCounter(input);
|
||||
scope.querySelectorAll('.contact-limit-counter').forEach((node) => node.remove());
|
||||
|
||||
if (input.dataset.counterBound === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
input.dataset.counterBound = 'true';
|
||||
input.addEventListener('input', () => updateContactCounter(input));
|
||||
});
|
||||
if (window.AdminFormHelpers) {
|
||||
window.AdminFormHelpers.refresh(scope);
|
||||
}
|
||||
}
|
||||
|
||||
function updateAllJsonInputs(data) {
|
||||
@@ -1327,7 +1275,6 @@
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" name="cardTitle_${index}" maxlength="40" data-maxlength="40">
|
||||
<small class="text-muted d-block mt-1">Recommended maximum: 40 characters.</small>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Icon Type</label>
|
||||
|
||||
@@ -404,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;
|
||||
@@ -709,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;
|
||||
}
|
||||
|
||||
@@ -780,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -446,92 +446,11 @@
|
||||
toast.addEventListener("hidden.bs.toast", () => toast.remove());
|
||||
}
|
||||
|
||||
function ensureCharacterHint(input) {
|
||||
if (!input || (!input.dataset.maxlength && !input.dataset.maxwords)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!input.id) {
|
||||
input.id = `homeField_${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
|
||||
const field = input.closest(".col-md-12, .col-md-9, .col-md-6, .col-md-4, .col-md-3, .col-lg-12, .col-lg-8, .col-lg-6, .col-lg-4, .col-12") || input.parentElement;
|
||||
const anchor = input.closest(".input-group") || input;
|
||||
const parent = anchor?.parentElement || field;
|
||||
if (!field || !anchor || !parent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let hint = field.querySelector(`[data-counter-for="${input.id}"]`);
|
||||
if (!hint) {
|
||||
hint = document.createElement("small");
|
||||
hint.className = "form-text home-limit-counter text-secondary";
|
||||
hint.dataset.counterFor = input.id;
|
||||
}
|
||||
|
||||
if (hint.previousElementSibling !== anchor) {
|
||||
parent.insertBefore(hint, anchor.nextSibling);
|
||||
}
|
||||
|
||||
return hint;
|
||||
}
|
||||
|
||||
function updateCharacterHint(input) {
|
||||
const hint = ensureCharacterHint(input);
|
||||
if (!hint) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasWordLimit = Boolean(input.dataset.maxwords);
|
||||
const hasCharLimit = Boolean(input.dataset.maxlength);
|
||||
|
||||
if (hasWordLimit) {
|
||||
const maxWords = Number(input.dataset.maxwords);
|
||||
const normalized = (input.value || "").replace(/\s+/g, " ").trim();
|
||||
const words = normalized ? normalized.split(" ") : [];
|
||||
|
||||
if (Number.isFinite(maxWords) && maxWords > 0 && words.length > maxWords) {
|
||||
input.value = words.slice(0, maxWords).join(" ");
|
||||
} else if (normalized !== input.value) {
|
||||
input.value = normalized;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCharLimit) {
|
||||
const max = Number(input.dataset.maxlength);
|
||||
if (Number.isFinite(max) && max > 0 && (input.value || "").length > max) {
|
||||
input.value = (input.value || "").slice(0, max);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasWordLimit) {
|
||||
const maxWords = Number(input.dataset.maxwords);
|
||||
const currentWords = input.value ? input.value.split(" ").filter(Boolean).length : 0;
|
||||
const currentLength = (input.value || "").length;
|
||||
const maxLength = Number(input.dataset.maxlength);
|
||||
hint.textContent = hasCharLimit
|
||||
? `${currentWords}/${maxWords} words, ${currentLength}/${maxLength} characters`
|
||||
: `${currentWords}/${maxWords} words`;
|
||||
hint.classList.toggle("text-danger", currentWords >= maxWords || (hasCharLimit && currentLength >= maxLength));
|
||||
return;
|
||||
}
|
||||
|
||||
const max = Number(input.dataset.maxlength);
|
||||
const length = (input.value || "").length;
|
||||
hint.textContent = `${length}/${max} characters`;
|
||||
hint.classList.toggle("text-danger", length >= max);
|
||||
}
|
||||
|
||||
function initHomeCharacterCounters(scope = document) {
|
||||
scope.querySelectorAll("input[data-maxlength], textarea[data-maxlength], input[data-maxwords], textarea[data-maxwords]").forEach((input) => {
|
||||
updateCharacterHint(input);
|
||||
scope.querySelectorAll(".home-limit-counter").forEach((node) => node.remove());
|
||||
|
||||
if (input.dataset.counterBound === "true") {
|
||||
return;
|
||||
}
|
||||
|
||||
input.dataset.counterBound = "true";
|
||||
input.addEventListener("input", () => updateCharacterHint(input));
|
||||
});
|
||||
if (window.AdminFormHelpers) {
|
||||
window.AdminFormHelpers.refresh(scope);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
style="height: 120px; width: 120px; object-fit: contain; background: #fff;"
|
||||
alt="Brand preview" />
|
||||
</div>
|
||||
<small class="text-muted d-block mt-2">Hiển thị thực tế khoảng 35x35px. Raster uploads are normalized to 104x104 WebP to match the homepage widget.</small>
|
||||
<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>
|
||||
@@ -95,7 +95,7 @@
|
||||
</div>
|
||||
<input type="hidden" id="floatingContactTriggerIconFallback"
|
||||
value="<%= data.floatingContact?.trigger?.icon || 'fa-comments' %>" />
|
||||
<small class="text-muted d-block mt-2">Hiển thị thực tế khoảng 26x26px. Shown only when the trigger slideshow cannot use the brand image and action icons. Raster uploads are normalized to 96x96 WebP.</small>
|
||||
<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>
|
||||
|
||||
@@ -9,18 +9,11 @@
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-light border mb-3">
|
||||
<div class="fw-semibold mb-1">Current homepage hero behavior</div>
|
||||
<div class="small text-muted">
|
||||
Mỗi slide dùng ảnh riêng làm nền full-width. Title, description và 2 button chỉ là lớp overlay.
|
||||
Trường video hiện không còn được hiển thị ở frontend.
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
Tùy chọn dự phòng. Khung hero desktop hiện hiển thị khoảng 1512x544px, nên nên upload ảnh ngang ít nhất 1920x700px.
|
||||
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"
|
||||
@@ -42,10 +35,10 @@
|
||||
<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>Dùng ảnh slide tỉ lệ ngang, ưu tiên ảnh lớn để fit container.</li>
|
||||
<li>Title ngắn 2-4 dòng để không tràn trên mobile.</li>
|
||||
<li>Description giữ ở mức 1-3 câu ngắn.</li>
|
||||
<li>Hai nút nên dùng link nội bộ như <code>/contact</code>.</li>
|
||||
<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>
|
||||
@@ -101,7 +94,7 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subtitle</label>
|
||||
<small class="text-muted d-block mb-1">Hiện không render ngoài frontend, chỉ giữ để tương thích dữ liệu cũ.</small>
|
||||
<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"
|
||||
maxlength="48" data-maxlength="48" />
|
||||
@@ -113,7 +106,7 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Slide Background Image</label>
|
||||
<small class="text-muted d-block mb-1">Ảnh này phủ toàn bộ hero. Khung desktop hiện khoảng 1512x544px, khuyến nghị upload 1920x700px hoặc lớn hơn.</small>
|
||||
<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="/uploads/home/hero-slide-01.jpg"
|
||||
@@ -132,9 +125,9 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Video URL</label>
|
||||
<small class="text-muted d-block mb-1">Frontend hiện không render video trong hero. Giữ lại chỉ để tránh mất dữ liệu cũ.</small>
|
||||
<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="Không bắt buộc"
|
||||
value="<%= slide.videoUrl || '' %>" placeholder="Optional"
|
||||
maxlength="255" data-maxlength="255" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<small class="text-muted d-block mb-3">Award icons trên homepage hiển thị khoảng 124x124px. Khuyến nghị upload ảnh vuông hoặc logo nền trong suốt tối thiểu 248x248px.</small>
|
||||
<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]) || {};
|
||||
@@ -31,7 +31,7 @@
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Icon / Logo</label>
|
||||
<small class="text-muted d-block mb-1">Khuyến nghị ảnh vuông hoặc logo nền trong suốt 248x248px để hiển thị sắc nét.</small>
|
||||
<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 || '' %>" maxlength="255" data-maxlength="255" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
@@ -64,7 +64,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<small class="text-muted d-block mb-3">Brand logo trong slider hiện hiển thị khoảng 159x48px. Khuyến nghị dùng SVG hoặc logo ngang nền trong suốt tối thiểu 320x96px.</small>
|
||||
<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">
|
||||
@@ -83,7 +83,7 @@
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-2">Khuyến nghị SVG hoặc ảnh ngang 320x96px để logo không bị mờ trong slider.</small>
|
||||
<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;">
|
||||
</div>
|
||||
@@ -143,7 +143,7 @@
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-2">Khuyến nghị SVG hoặc ảnh ngang 320x96px để logo không bị mờ trong slider.</small>
|
||||
<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>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Video Thumbnail</label>
|
||||
<small class="text-muted d-block mb-1">Khung thumbnail desktop hiện khoảng 416x370px. Khuyến nghị upload tối thiểu 832x740px.</small>
|
||||
<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 || '' %>" maxlength="255" data-maxlength="255" />
|
||||
@@ -100,7 +100,7 @@
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Avatar</label>
|
||||
<small class="text-muted d-block mb-1">Avatar hiển thị ở khoảng 48x48px. Khuyến nghị ảnh vuông 96x96px hoặc 128x128px.</small>
|
||||
<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 || '' %>" maxlength="255" data-maxlength="255" />
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Flag / Illustration Image</label>
|
||||
<small class="text-muted d-block mb-1">Khung ảnh desktop hiện khoảng 840x830px. Khuyến nghị ảnh gần vuông, tối thiểu 1000x1000px.</small>
|
||||
<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"
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<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">Khung desktop hiện khoảng 318x347px. Khuyến nghị upload ít nhất 750x820px, tỉ lệ dọc khoảng 0.91:1.</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"
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Secondary Image</label>
|
||||
<small class="text-muted d-block mb-1">Khung desktop hiện khoảng 363x380px. Khuyến nghị upload ít nhất 760x800px, tỉ lệ dọc khoảng 0.95:1.</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 || '' %>"
|
||||
@@ -113,7 +113,7 @@
|
||||
<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">Icon hiển thị ở khoảng 24x24px. Khuyến nghị dùng SVG hoặc ảnh vuông 48x48px.</small>
|
||||
<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 || '' %>" maxlength="255" data-maxlength="255" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
|
||||
<!-- Form helpers and shared admin UI styles -->
|
||||
<link rel="stylesheet" href="/assets/css/components/form.css" />
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #b8b76a;
|
||||
@@ -42,7 +44,7 @@
|
||||
<%- style %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body data-admin-helpers="true">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<nav class="col-md-2 d-none d-md-block bg-light sidebar">
|
||||
@@ -117,6 +119,7 @@
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/admin-form-helpers.js"></script>
|
||||
<script>
|
||||
// Global function to clean up any stuck modal backdrops
|
||||
function forceCleanupModals() {
|
||||
@@ -153,4 +156,4 @@
|
||||
<%- script %>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -676,7 +676,7 @@
|
||||
<%- style %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body data-admin-helpers="true">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg navbar-light sticky-top">
|
||||
<div class="container">
|
||||
@@ -821,6 +821,9 @@
|
||||
<!-- Custom modal enhancement -->
|
||||
<script src="/js/custom-modal.js"></script>
|
||||
|
||||
<!-- Shared admin form helpers -->
|
||||
<script src="/js/admin-form-helpers.js"></script>
|
||||
|
||||
<script>
|
||||
// Load Level Types cho dropdown menu Academics với submenu programmes
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
@@ -1149,4 +1152,4 @@
|
||||
<%- script %>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user