forked from UKSOURCE/cms.hailearning.edu.vn
feat: Improve home and contact CMS field guidance
This commit is contained in:
@@ -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">The contact hero currently renders at about 1496x544px on desktop. 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,9 @@
|
||||
<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">
|
||||
<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>
|
||||
@@ -324,7 +329,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 +353,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 +361,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 +389,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 +472,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 +521,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 +549,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 +566,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 +789,7 @@
|
||||
|
||||
updateAllJsonInputs(originalFormData);
|
||||
initializeFormHandlers();
|
||||
initContactCharacterCounters(document);
|
||||
});
|
||||
|
||||
function applyDateFilter() {
|
||||
@@ -1195,6 +1210,65 @@
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (input.dataset.counterBound === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
input.dataset.counterBound = 'true';
|
||||
input.addEventListener('input', () => updateContactCounter(input));
|
||||
});
|
||||
}
|
||||
|
||||
function updateAllJsonInputs(data) {
|
||||
document.getElementById('heroJson').value = JSON.stringify(data.hero || {});
|
||||
document.getElementById('contactCardsJson').value = JSON.stringify(data.contactCards || []);
|
||||
@@ -1252,7 +1326,8 @@
|
||||
</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">
|
||||
<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>
|
||||
@@ -1306,20 +1381,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 +1415,8 @@
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
}
|
||||
|
||||
initContactCharacterCounters(newCard);
|
||||
}
|
||||
|
||||
function handleIconSourceChange(radio) {
|
||||
@@ -1385,7 +1462,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 +1477,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 +1487,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 +1501,7 @@
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
initContactCharacterCounters(container.lastElementChild);
|
||||
}
|
||||
|
||||
function removeFormField(button) {
|
||||
@@ -1662,4 +1741,4 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user