feat: standardize admin form limits and guidance

This commit is contained in:
Tống Thành Đạt
2026-04-10 15:55:15 +07:00
parent 7ce5921fe0
commit 51c6303437
34 changed files with 1692 additions and 361 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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