feat: enhance about us and footer CMS admin panels

- Improve aboutUs controller with better field handling
- Update footer controller with expanded content management
- Refine about admin view templates
- Update appointment and footer admin views
- Add about contract repair migration script
- Update about.json seed data
This commit is contained in:
Tống Thành Đạt
2026-04-10 22:32:51 +07:00
parent 51c6303437
commit c6a2d4a55d
8 changed files with 534 additions and 114 deletions

View File

@@ -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>
@@ -89,6 +89,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"
@@ -134,6 +135,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"
@@ -180,15 +182,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>
@@ -196,11 +203,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">
@@ -245,6 +253,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>
@@ -254,6 +263,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>
@@ -272,6 +282,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>
@@ -320,7 +331,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 %>
@@ -403,7 +414,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
@@ -635,7 +646,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) {
@@ -706,18 +717,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>
@@ -730,12 +733,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 || '';
});
}
@@ -890,14 +892,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 !== ''),
@@ -964,4 +962,4 @@
transform: translateY(-3px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
</style>
</style>