forked from UKSOURCE/cms.hailearning.edu.vn
first commit
This commit is contained in:
@@ -1,967 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form method="POST" class="content-with-fixed-buttons" id="aboutUsForm"
|
||||
action="/admin/about-us/update">
|
||||
<!-- Hidden inputs for JSON data -->
|
||||
<input type="hidden" name="heroJson" id="heroJson">
|
||||
<input type="hidden" name="introJson" id="introJson">
|
||||
<input type="hidden" name="missionJson" id="missionJson">
|
||||
<input type="hidden" name="featuresJson" id="featuresJson">
|
||||
<input type="hidden" name="newsJson" id="newsJson">
|
||||
<input type="hidden" name="activeTab" id="activeTabInput" value="<%= locals.activeTab || 'hero' %>">
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<!-- Tab Menu -->
|
||||
<ul class="nav nav-tabs card-header-tabs" id="aboutUsTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link <%= (locals.activeTab === 'hero' || !locals.activeTab) ? 'active' : '' %>"
|
||||
id="hero-tab" data-bs-toggle="tab" href="#hero" role="tab"
|
||||
aria-selected="true"><i class="fas fa-image me-2"></i>Hero</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link <%= locals.activeTab === 'intro' ? 'active' : '' %>" id="intro-tab"
|
||||
data-bs-toggle="tab" href="#intro" role="tab"
|
||||
aria-selected="false"><i class="fas fa-info-circle me-2"></i>Intro</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link <%= locals.activeTab === 'mission' ? 'active' : '' %>" id="mission-tab"
|
||||
data-bs-toggle="tab" href="#mission" role="tab"
|
||||
aria-selected="false"><i class="fas fa-bullseye me-2"></i>Mission</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link <%= locals.activeTab === 'features' ? 'active' : '' %>" id="features-tab"
|
||||
data-bs-toggle="tab" href="#features" role="tab"
|
||||
aria-selected="false"><i class="fas fa-star me-2"></i>Features</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link <%= locals.activeTab === 'news' ? 'active' : '' %>" id="news-tab"
|
||||
data-bs-toggle="tab" href="#news" role="tab"
|
||||
aria-selected="false"><i class="fas fa-newspaper me-2"></i>News</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<!-- Hero Tab -->
|
||||
<div class="tab-pane fade <%= (locals.activeTab === 'hero' || !locals.activeTab) ? 'show active' : '' %>" id="hero"
|
||||
role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="fas fa-image me-2"></i>Hero Section</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="heroTitle" name="heroTitle"
|
||||
value="<%= data.hero?.title || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Breadcrumb (comma separated)</label>
|
||||
<input type="text" class="form-control" id="heroBreadcrumb"
|
||||
value="<%= (data.hero?.breadcrumb || []).join(', ') %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Background Image</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="heroBackgroundImage"
|
||||
name="heroBackgroundImage" value="<%= data.hero?.backgroundImage || '' %>">
|
||||
<button class="btn btn-outline-primary btn-upload-image" type="button"
|
||||
data-target-input="heroBackgroundImage" data-image-type="about">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<% if (data.hero?.backgroundImage) { %>
|
||||
<img src="<%= data.hero.backgroundImage %>"
|
||||
class="img-thumbnail uploaded-preview mt-2"
|
||||
style="max-height: 200px;">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Intro Tab -->
|
||||
<div class="tab-pane fade <%= locals.activeTab === 'intro' ? 'show active' : '' %>" id="intro"
|
||||
role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="fas fa-info-circle me-2"></i>Introduction</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Subheading</label>
|
||||
<input type="text" class="form-control" id="introSubheading" name="introSubheading"
|
||||
value="<%= data.intro?.subheading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Heading</label>
|
||||
<input type="text" class="form-control" id="introHeading" name="introHeading"
|
||||
value="<%= data.intro?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="introDescription" name="introDescription"
|
||||
rows="4"><%= data.intro?.description || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Main Image</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="introImage" name="introImage"
|
||||
value="<%= data.intro?.image || '' %>">
|
||||
<button class="btn btn-outline-primary btn-upload-image" type="button"
|
||||
data-target-input="introImage" data-image-type="about">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<% if (data.intro?.image) { %>
|
||||
<img src="<%= data.intro.image %>"
|
||||
class="img-thumbnail uploaded-preview mt-2"
|
||||
style="max-height: 200px;">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mission Tab -->
|
||||
<div class="tab-pane fade <%= locals.activeTab === 'mission' ? 'show active' : '' %>" id="mission" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="fas fa-bullseye me-2"></i>Mission Section</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Subheading</label>
|
||||
<input type="text" class="form-control" id="missionSubheading" value="<%= data.mission?.subheading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Heading</label>
|
||||
<input type="text" class="form-control" id="missionHeading" value="<%= data.mission?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="missionDescription" rows="3"><%= data.mission?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">CTA Button Label</label>
|
||||
<input type="text" class="form-control" id="missionCtaLabel" value="<%= data.mission?.ctaButton?.label || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">CTA Button Link</label>
|
||||
<input type="text" class="form-control" id="missionCtaHref" value="<%= data.mission?.ctaButton?.href || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 class="mt-4 mb-3">Images</h6>
|
||||
<div class="row g-3">
|
||||
<% ['main', 'secondary', 'bgShape', 'planeShape', 'topShape', 'globeShape'].forEach(imgKey => { %>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label"><%= imgKey.charAt(0).toUpperCase() + imgKey.slice(1) %></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>
|
||||
</div>
|
||||
<% }) %>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addMissionItem()">
|
||||
<i class="fas fa-plus me-1"></i>Add
|
||||
</button>
|
||||
</div>
|
||||
<div id="missionItemsContainer"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label mb-0">Features (List)</label>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addMissionFeature()">
|
||||
<i class="fas fa-plus me-1"></i>Add
|
||||
</button>
|
||||
</div>
|
||||
<div id="missionFeaturesContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features Tab -->
|
||||
<div class="tab-pane fade <%= locals.activeTab === 'features' ? 'show active' : '' %>" id="features" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="fas fa-star me-2"></i>Features Section</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Subheading</label>
|
||||
<input type="text" class="form-control" id="featuresSubheading" value="<%= data.features?.subheading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Heading</label>
|
||||
<input type="text" class="form-control" id="featuresHeading" value="<%= data.features?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="featuresDescription" rows="3"><%= data.features?.description || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Background Image</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="featuresBgImage" value="<%= data.features?.backgroundImage || '' %>">
|
||||
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="featuresBgImage" data-image-type="about">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Side Image</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="featuresImage" value="<%= data.features?.image || '' %>">
|
||||
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="featuresImage" data-image-type="about">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">CTA Button Label</label>
|
||||
<input type="text" class="form-control" id="featuresCtaLabel" value="<%= data.features?.ctaButton?.label || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">CTA Button Link</label>
|
||||
<input type="text" class="form-control" id="featuresCtaHref" value="<%= data.features?.ctaButton?.href || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Feature Items</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addFeatureItem()">
|
||||
<i class="fas fa-plus me-1"></i>Add Item
|
||||
</button>
|
||||
</div>
|
||||
<div id="featureItemsContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- News Tab -->
|
||||
<div class="tab-pane fade <%= locals.activeTab === 'news' ? 'show active' : '' %>" id="news" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0"><i class="fas fa-newspaper me-2"></i>News Section (Blog Preview)</h6>
|
||||
<span class="badge bg-info text-dark">System will automatically fetch the 3 latest posts if no specific blog is selected.</span>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subheading</label>
|
||||
<input type="text" class="form-control" id="newsSubheading" value="<%= data.news?.subheading || '' %>" placeholder="e.g., Visa Tips & Guides">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input type="text" class="form-control" id="newsHeading" value="<%= data.news?.heading || '' %>" placeholder="e.g., Latest Insights & Updates">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">CTA Button Label</label>
|
||||
<input type="text" class="form-control" id="newsCtaLabel" value="<%= data.news?.ctaButton?.label || '' %>" placeholder="e.g., View All Articles">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">CTA Button Link</label>
|
||||
<input type="text" class="form-control" id="newsCtaHref" value="<%= data.news?.ctaButton?.href || '' %>" placeholder="/blog">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 mt-4">
|
||||
<label class="form-label fw-bold"><i class="fas fa-check-square me-2"></i>Select Featured Blogs (Direct from Blog Module)</label>
|
||||
<p class="text-muted small mb-3">Select blog posts to display on About page. If none are selected, the system will use the 3 latest posts.</p>
|
||||
<div class="row g-3 blog-selector-container" style="max-height: 400px; overflow-y: auto; border: 1px solid #eee; padding: 15px; border-radius: 8px;">
|
||||
<% if (allBlogs && allBlogs.length > 0) { %>
|
||||
<% allBlogs.forEach(blog => {
|
||||
const isSelected = data.news?.selectedBlogIds && data.news.selectedBlogIds.some(id => id.toString() === blog._id.toString());
|
||||
%>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 blog-select-card <%= isSelected ? 'border-primary bg-light' : '' %>" onclick="toggleAboutBlogSelection(this, '<%= blog._id %>')" style="cursor: pointer; transition: all 0.2s;">
|
||||
<div class="position-absolute top-0 end-0 m-2">
|
||||
<div class="form-check">
|
||||
<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;">
|
||||
<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 %>
|
||||
</h6>
|
||||
<p class="card-text tiny text-muted mb-0">
|
||||
<%= blog.publishedAt ? new Date(blog.publishedAt).toLocaleDateString('vi-VN') : '' %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } else { %>
|
||||
<div class="col-12 text-center py-4">
|
||||
<p class="text-muted">No published blogs found. Please create some blogs first.</p>
|
||||
<a href="/admin/blog/create" class="btn btn-sm btn-outline-primary">Create Blog</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fixed Bottom Buttons inside Card Footer -->
|
||||
<div class="card-footer bg-light d-flex justify-content-end py-3 gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary px-4" onclick="resetForm()">
|
||||
<i class="fas fa-undo me-2"></i>Reset
|
||||
</button>
|
||||
<button type="submit" class="btn btn-outline-primary px-4" id="submitBtn">
|
||||
<i class="fas fa-save me-2"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let originalFormData = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
originalFormData = <%- JSON.stringify(data) %>;
|
||||
updateAllJsonInputs(originalFormData);
|
||||
initializeFormHandlers();
|
||||
});
|
||||
|
||||
function initializeFormHandlers() {
|
||||
const form = document.getElementById('aboutUsForm');
|
||||
form.addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const originalHtml = submitBtn.innerHTML;
|
||||
|
||||
try {
|
||||
// Collect all data into a single object
|
||||
updateJsonData();
|
||||
// No more "Saving..." text or disabling button to keep UI "instant"
|
||||
// submitBtn.disabled = true;
|
||||
// submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
|
||||
|
||||
// We'll send the individual section JSONs or construct one big one
|
||||
const aboutData = {
|
||||
hero: JSON.parse(document.getElementById('heroJson').value),
|
||||
intro: JSON.parse(document.getElementById('introJson').value),
|
||||
mission: JSON.parse(document.getElementById('missionJson').value),
|
||||
features: JSON.parse(document.getElementById('featuresJson').value),
|
||||
news: JSON.parse(document.getElementById('newsJson').value)
|
||||
};
|
||||
|
||||
const response = await fetch('/api/about', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ aboutJson: JSON.stringify(aboutData) })
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Update failed');
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
showToast('Success', 'About Us updated successfully', 'success');
|
||||
|
||||
// Update the local state with returned data from server
|
||||
// This ensures the UI is in sync with what was actually saved
|
||||
if (result.data) {
|
||||
originalFormData = result.data;
|
||||
updateAllJsonInputs(originalFormData);
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to update');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Error', error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener('click', function (e) {
|
||||
const uploadBtn = e.target.closest('.btn-upload-image');
|
||||
if (uploadBtn) {
|
||||
const targetInput = uploadBtn.dataset.targetInput;
|
||||
const imageType = uploadBtn.dataset.imageType;
|
||||
openImageUploader(targetInput, imageType);
|
||||
}
|
||||
});
|
||||
|
||||
// Tab change listener to keep track of active tab
|
||||
const tabs = document.querySelectorAll('a[data-bs-toggle="tab"]');
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('shown.bs.tab', function (e) {
|
||||
const targetId = e.target.getAttribute('href').replace('#', '');
|
||||
const activeTabInput = document.getElementById('activeTabInput');
|
||||
if (activeTabInput) {
|
||||
activeTabInput.value = targetId;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openImageUploader(targetInput, imageType) {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.style.display = 'none';
|
||||
document.body.appendChild(fileInput);
|
||||
|
||||
function getPreviewDims(name) {
|
||||
if (/hero/i.test(name)) return { h: '250px', w: '100%' };
|
||||
if (/intro/i.test(name) || /mission/i.test(name) || /features/i.test(name)) return { h: '150px', w: '100%' };
|
||||
return { h: '120px', w: '100%' };
|
||||
}
|
||||
|
||||
fileInput.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
// Disable upload button during upload
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : 'Upload';
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = true;
|
||||
}
|
||||
|
||||
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Upload failed');
|
||||
}
|
||||
|
||||
// Update input value
|
||||
const input = document.getElementById(targetInput) || document.querySelector(`[name="${targetInput}"]`);
|
||||
if (!input) {
|
||||
throw new Error('Target input not found');
|
||||
}
|
||||
|
||||
// Use absolute URL for preview when necessary
|
||||
const previewUrl = (result.path && (result.path.startsWith('http://') || result.path.startsWith('https://'))) ? result.path : (window.location.origin + result.path);
|
||||
|
||||
input.value = result.path;
|
||||
|
||||
// Try to find an existing image preview inside the closest card or input group (prefer .uploaded-preview)
|
||||
let card = input.closest('.card');
|
||||
let previewImg = card ? card.querySelector('.uploaded-preview') : null;
|
||||
|
||||
if (!previewImg) {
|
||||
// Look for a preview right after the input group
|
||||
const parent = input.parentElement || input.closest('.input-group') || input.closest('div');
|
||||
previewImg = parent ? parent.querySelector('.uploaded-preview') : null;
|
||||
}
|
||||
|
||||
const dims = getPreviewDims(targetInput);
|
||||
|
||||
if (previewImg) {
|
||||
previewImg.src = previewUrl;
|
||||
previewImg.style.height = dims.h;
|
||||
previewImg.style.width = dims.w;
|
||||
} else {
|
||||
// Create a preview image and attach it after the input group
|
||||
const img = document.createElement('img');
|
||||
img.src = previewUrl;
|
||||
img.className = 'img-thumbnail uploaded-preview mt-2';
|
||||
img.style.height = dims.h;
|
||||
img.style.width = dims.w;
|
||||
img.style.objectFit = 'cover';
|
||||
img.alt = 'Image preview';
|
||||
|
||||
const parent = input.parentElement || input.closest('.input-group') || input.closest('div');
|
||||
if (parent) parent.appendChild(img);
|
||||
}
|
||||
|
||||
// Removed toast for silent upload
|
||||
|
||||
// Restore button state
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
showToast('Error', 'Failed to upload image: ' + error.message, 'error');
|
||||
|
||||
// Restore button state
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
}
|
||||
} finally {
|
||||
document.body.removeChild(fileInput);
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.click();
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
if (confirm('Are you sure you want to reset all changes?')) {
|
||||
updateAllJsonInputs(originalFormData);
|
||||
showToast('Reset', 'Form restored to last saved state', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const alertHtml = `
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
`;
|
||||
document.querySelector('.container').insertAdjacentHTML('afterbegin', alertHtml);
|
||||
}
|
||||
|
||||
// Show toast message
|
||||
function showToast(title, message, type = 'info') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast align-items-center text-white bg-${type === 'error' ? 'danger' : type} border-0`;
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.setAttribute('aria-live', 'assertive');
|
||||
toast.setAttribute('aria-atomic', 'true');
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<strong>${title}:</strong> ${message}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add toast to container
|
||||
let container = document.querySelector('.toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.className = 'toast-container position-fixed top-0 end-0 p-3';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
container.appendChild(toast);
|
||||
|
||||
// Show toast
|
||||
const bsToast = new bootstrap.Toast(toast, {
|
||||
animation: true,
|
||||
autohide: true,
|
||||
delay: 3000
|
||||
});
|
||||
bsToast.show();
|
||||
|
||||
// Remove toast after hide
|
||||
toast.addEventListener('hidden.bs.toast', () => {
|
||||
toast.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function updateAllJsonInputs(data) {
|
||||
if (!data) return;
|
||||
|
||||
// 1. Hero
|
||||
const hero = data.hero || {};
|
||||
document.getElementById('heroJson').value = JSON.stringify(hero);
|
||||
document.getElementById('heroTitle').value = hero.title || '';
|
||||
document.getElementById('heroBreadcrumb').value = (hero.breadcrumb || []).join(', ');
|
||||
document.getElementById('heroBackgroundImage').value = hero.backgroundImage || '';
|
||||
updateImagePreview('heroBackgroundImage', hero.backgroundImage);
|
||||
|
||||
// 2. Intro
|
||||
const intro = data.intro || {};
|
||||
document.getElementById('introJson').value = JSON.stringify(intro);
|
||||
document.getElementById('introSubheading').value = intro.subheading || '';
|
||||
document.getElementById('introHeading').value = intro.heading || '';
|
||||
document.getElementById('introDescription').value = intro.description || '';
|
||||
document.getElementById('introImage').value = intro.image || '';
|
||||
updateImagePreview('introImage', intro.image);
|
||||
|
||||
// 3. Mission
|
||||
const mission = data.mission || {};
|
||||
document.getElementById('missionJson').value = JSON.stringify(mission);
|
||||
document.getElementById('missionSubheading').value = mission.subheading || '';
|
||||
document.getElementById('missionHeading').value = mission.heading || '';
|
||||
document.getElementById('missionDescription').value = mission.description || '';
|
||||
document.getElementById('missionCtaLabel').value = mission.ctaButton?.label || '';
|
||||
document.getElementById('missionCtaHref').value = mission.ctaButton?.href || '';
|
||||
|
||||
['main', 'secondary', 'bgShape', 'planeShape', 'topShape', 'globeShape'].forEach(k => {
|
||||
const el = document.getElementById('missionImg_' + k);
|
||||
const val = mission.images?.[k] || '';
|
||||
if (el) {
|
||||
el.value = val;
|
||||
updateImagePreview('missionImg_' + k, val);
|
||||
}
|
||||
});
|
||||
populateMissionItems(mission.items || []);
|
||||
populateMissionFeatures(mission.features || []);
|
||||
|
||||
// 4. Features
|
||||
const features = data.features || {};
|
||||
document.getElementById('featuresJson').value = JSON.stringify(features);
|
||||
document.getElementById('featuresSubheading').value = features.subheading || '';
|
||||
document.getElementById('featuresHeading').value = features.heading || '';
|
||||
document.getElementById('featuresDescription').value = features.description || '';
|
||||
document.getElementById('featuresBgImage').value = features.backgroundImage || '';
|
||||
document.getElementById('featuresImage').value = features.image || '';
|
||||
document.getElementById('featuresCtaLabel').value = features.ctaButton?.label || '';
|
||||
document.getElementById('featuresCtaHref').value = features.ctaButton?.href || '';
|
||||
updateImagePreview('featuresBgImage', features.backgroundImage);
|
||||
updateImagePreview('featuresImage', features.image);
|
||||
populateFeatureItems(features.items || []);
|
||||
|
||||
// 5. News
|
||||
const news = data.news || {};
|
||||
document.getElementById('newsJson').value = JSON.stringify(news);
|
||||
document.getElementById('newsSubheading').value = news.subheading || '';
|
||||
document.getElementById('newsHeading').value = news.heading || '';
|
||||
document.getElementById('newsCtaLabel').value = news.ctaButton?.label || '';
|
||||
document.getElementById('newsCtaHref').value = news.ctaButton?.href || '';
|
||||
|
||||
// Update blog selection checkboxes
|
||||
document.querySelectorAll('.about-blog-checkbox').forEach(cb => {
|
||||
const isSelected = news.selectedBlogIds && news.selectedBlogIds.some(id => id.toString() === cb.value);
|
||||
cb.checked = isSelected;
|
||||
const card = cb.closest('.blog-select-card');
|
||||
if (card) {
|
||||
handleAboutCheckboxUpdate(card, isSelected);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateImagePreview(inputId, imagePath) {
|
||||
if (!imagePath) return;
|
||||
const input = document.getElementById(inputId);
|
||||
if (!input) return;
|
||||
|
||||
let card = input.closest('.card');
|
||||
let previewImg = card ? card.querySelector('.uploaded-preview') : null;
|
||||
|
||||
if (!previewImg) {
|
||||
const parent = input.closest('.input-group') || input.parentElement;
|
||||
previewImg = parent ? parent.querySelector('.uploaded-preview') : null;
|
||||
}
|
||||
|
||||
if (previewImg) {
|
||||
previewImg.src = imagePath;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helper dynamic populations ---
|
||||
|
||||
function addMissionItem() {
|
||||
const container = document.getElementById('missionItemsContainer');
|
||||
const idx = container.children.length;
|
||||
const html = `
|
||||
<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">
|
||||
<input type="text" class="form-control form-control-sm" name="missionItemLabel_${idx}" placeholder="Label">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control form-control-sm" name="missionItemDesc_${idx}" placeholder="Description">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-1" onclick="this.closest('.mission-item').remove()">Remove</button>
|
||||
</div>
|
||||
</div>`;
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function populateMissionItems(items) {
|
||||
const container = document.getElementById('missionItemsContainer');
|
||||
container.innerHTML = '';
|
||||
items.forEach((item, i) => {
|
||||
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 || '';
|
||||
});
|
||||
}
|
||||
|
||||
function addMissionFeature() {
|
||||
const container = document.getElementById('missionFeaturesContainer');
|
||||
const html = `
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<input type="text" class="form-control mission-feature-input" placeholder="Feature text">
|
||||
<button class="btn btn-outline-danger" type="button" onclick="this.parentElement.remove()">x</button>
|
||||
</div>`;
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function populateMissionFeatures(features) {
|
||||
const container = document.getElementById('missionFeaturesContainer');
|
||||
container.innerHTML = '';
|
||||
features.forEach(f => {
|
||||
addMissionFeature();
|
||||
container.lastElementChild.querySelector('input').value = f || '';
|
||||
});
|
||||
}
|
||||
|
||||
function addFeatureItem() {
|
||||
const container = document.getElementById('featureItemsContainer');
|
||||
const idx = container.children.length;
|
||||
const html = `
|
||||
<div class="card mb-2 feature-item-row">
|
||||
<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="featureItemIcon_${idx}" placeholder="Icon path">
|
||||
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="featureItemIcon_${idx}" data-image-type="about">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control form-control-sm" name="featureItemTitle_${idx}" placeholder="Title">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control form-control-sm" name="featureItemDesc_${idx}" placeholder="Description">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-1" onclick="this.closest('.feature-item-row').remove()">Remove</button>
|
||||
</div>
|
||||
</div>`;
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function populateFeatureItems(items) {
|
||||
const container = document.getElementById('featureItemsContainer');
|
||||
container.innerHTML = '';
|
||||
items.forEach((item, i) => {
|
||||
addFeatureItem();
|
||||
const last = container.lastElementChild;
|
||||
last.querySelector(`[name="featureItemIcon_${i}"]`).value = item.icon || '';
|
||||
last.querySelector(`[name="featureItemTitle_${i}"]`).value = item.title || '';
|
||||
last.querySelector(`[name="featureItemDesc_${i}"]`).value = item.description || '';
|
||||
});
|
||||
}
|
||||
|
||||
// Blog selection functions for About News section
|
||||
function toggleAboutBlogSelection(card, blogId) {
|
||||
const checkbox = card.querySelector('.about-blog-checkbox');
|
||||
const isChecking = !checkbox.checked;
|
||||
|
||||
if (isChecking) {
|
||||
const checkedCount = document.querySelectorAll('.about-blog-checkbox:checked').length;
|
||||
if (checkedCount >= 3) {
|
||||
alert('You can only select up to 3 blogs.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
checkbox.checked = isChecking;
|
||||
handleAboutCheckboxUpdate(card, checkbox.checked);
|
||||
}
|
||||
|
||||
function handleAboutCheckboxChange(checkbox) {
|
||||
if (checkbox.checked) {
|
||||
const checkedCount = document.querySelectorAll('.about-blog-checkbox:checked').length;
|
||||
if (checkedCount > 3) {
|
||||
checkbox.checked = false;
|
||||
alert('You can only select up to 3 blogs.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
const card = checkbox.closest('.blog-select-card');
|
||||
handleAboutCheckboxUpdate(card, checkbox.checked);
|
||||
}
|
||||
|
||||
function handleAboutCheckboxUpdate(card, isChecked) {
|
||||
if (isChecked) {
|
||||
card.classList.add('border-primary', 'bg-light');
|
||||
} else {
|
||||
card.classList.remove('border-primary', 'bg-light');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function addService() {
|
||||
const container = document.getElementById('servicesContainer');
|
||||
const index = container.children.length;
|
||||
const html = `
|
||||
<div class="card mb-3 service-item">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" name="serviceTitle_${index}">
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" name="serviceDescription_${index}" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeService(this)">
|
||||
<i class="fas fa-trash me-2"></i>Remove Service
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
|
||||
function updateJsonData() {
|
||||
try {
|
||||
// Hero
|
||||
const heroData = {
|
||||
title: document.getElementById('heroTitle').value.trim(),
|
||||
breadcrumb: document.getElementById('heroBreadcrumb').value.split(',').map(s => s.trim()).filter(s => s !== ''),
|
||||
backgroundImage: document.getElementById('heroBackgroundImage').value.trim()
|
||||
};
|
||||
document.getElementById('heroJson').value = JSON.stringify(heroData);
|
||||
|
||||
// Intro
|
||||
const introData = {
|
||||
subheading: document.getElementById('introSubheading').value.trim(),
|
||||
heading: document.getElementById('introHeading').value.trim(),
|
||||
description: document.getElementById('introDescription').value.trim(),
|
||||
image: document.getElementById('introImage').value.trim()
|
||||
};
|
||||
document.getElementById('introJson').value = JSON.stringify(introData);
|
||||
|
||||
// Mission
|
||||
const missionData = {
|
||||
subheading: document.getElementById('missionSubheading').value.trim(),
|
||||
heading: document.getElementById('missionHeading').value.trim(),
|
||||
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()
|
||||
},
|
||||
items: Array.from(document.querySelectorAll('.mission-item')).map(item => ({
|
||||
icon: item.querySelector('[name^="missionItemIcon_"]').value.trim(),
|
||||
label: item.querySelector('[name^="missionItemLabel_"]').value.trim(),
|
||||
description: item.querySelector('[name^="missionItemDesc_"]').value.trim()
|
||||
})).filter(i => i.label !== ''),
|
||||
features: Array.from(document.querySelectorAll('.mission-feature-input')).map(input => input.value.trim()).filter(v => v !== ''),
|
||||
ctaButton: {
|
||||
label: document.getElementById('missionCtaLabel').value.trim(),
|
||||
href: document.getElementById('missionCtaHref').value.trim()
|
||||
}
|
||||
};
|
||||
document.getElementById('missionJson').value = JSON.stringify(missionData);
|
||||
|
||||
// Features
|
||||
const featuresData = {
|
||||
backgroundImage: document.getElementById('featuresBgImage').value.trim(),
|
||||
subheading: document.getElementById('featuresSubheading').value.trim(),
|
||||
heading: document.getElementById('featuresHeading').value.trim(),
|
||||
description: document.getElementById('featuresDescription').value.trim(),
|
||||
image: document.getElementById('featuresImage').value.trim(),
|
||||
items: Array.from(document.querySelectorAll('.feature-item-row')).map(item => ({
|
||||
icon: item.querySelector('[name^="featureItemIcon_"]').value.trim(),
|
||||
title: item.querySelector('[name^="featureItemTitle_"]').value.trim(),
|
||||
description: item.querySelector('[name^="featureItemDesc_"]').value.trim()
|
||||
})).filter(i => i.title !== ''),
|
||||
ctaButton: {
|
||||
label: document.getElementById('featuresCtaLabel').value.trim(),
|
||||
href: document.getElementById('featuresCtaHref').value.trim()
|
||||
}
|
||||
};
|
||||
document.getElementById('featuresJson').value = JSON.stringify(featuresData);
|
||||
|
||||
// News
|
||||
const selectedIds = [];
|
||||
document.querySelectorAll('.about-blog-checkbox:checked').forEach(cb => {
|
||||
selectedIds.push(cb.value);
|
||||
});
|
||||
|
||||
const newsData = {
|
||||
subheading: document.getElementById('newsSubheading').value.trim(),
|
||||
heading: document.getElementById('newsHeading').value.trim(),
|
||||
ctaButton: {
|
||||
label: document.getElementById('newsCtaLabel').value.trim(),
|
||||
href: document.getElementById('newsCtaHref').value.trim()
|
||||
},
|
||||
selectedBlogIds: selectedIds,
|
||||
items: [] // Server will populate this from selectedBlogIds
|
||||
};
|
||||
document.getElementById('newsJson').value = JSON.stringify(newsData);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating JSON data:', error);
|
||||
throw new Error('Failed to process form data');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tiny {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.blog-select-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,791 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
|
||||
<%= title %>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Edit content displayed on Appointment page</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="<%= frontendUrl %>/make-appointment/" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-2"></i>View Appointment Page
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form method="POST" class="content-with-fixed-buttons" id="appointmentForm"
|
||||
action="/admin/appointment/update">
|
||||
<!-- Hidden inputs for JSON data -->
|
||||
<input type="hidden" name="hero" id="heroJson">
|
||||
<input type="hidden" name="visaOptions" id="visaOptionsJson">
|
||||
<input type="hidden" name="form" id="formJson">
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#hero" role="tab">
|
||||
<i class="fas fa-home me-2"></i>Hero
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#visaOptions" role="tab">
|
||||
<i class="fas fa-passport me-2"></i>Visa Options
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#form" role="tab">
|
||||
<i class="fas fa-envelope me-2"></i>Form
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#submissions" role="tab">
|
||||
<i class="fas fa-list me-2"></i>Submissions
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<!-- Hero Tab -->
|
||||
<div class="tab-pane fade show active" id="hero" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-body">
|
||||
<h6 class="fw-medium mb-3">Hero Section</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label fw-medium">Background Image</label>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="heroBackgroundImage"
|
||||
name="heroBackgroundImage"
|
||||
value="<%= data.hero?.backgroundImage || '' %>">
|
||||
<button type="button"
|
||||
class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="heroBackgroundImage"
|
||||
data-image-type="appointment">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted">Recommended size: 1920x1080px</small>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div id="heroImagePreview" style="height: 200px;">
|
||||
<% if (data.hero?.backgroundImage) { %>
|
||||
<% let heroImgSrc=data.hero.backgroundImage; if (heroImgSrc &&
|
||||
!heroImgSrc.startsWith('http://') &&
|
||||
!heroImgSrc.startsWith('https://')) {
|
||||
heroImgSrc=heroImgSrc.startsWith('/') ? heroImgSrc : '/' +
|
||||
heroImgSrc; } %>
|
||||
<img src="<%= heroImgSrc %>" class="img-thumbnail"
|
||||
id="heroPreviewImg"
|
||||
style="height: 200px; width: 100%; object-fit: cover;"
|
||||
alt="Background image preview"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
||||
<div class="border rounded p-5 text-center text-muted"
|
||||
style="height: 200px; display: none; align-items: center; justify-content: center;">
|
||||
Image preview
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="border rounded p-5 text-center text-muted"
|
||||
style="height: 200px; display: flex; align-items: center; justify-content: center;">
|
||||
Image preview
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="heroTitle" name="heroTitle"
|
||||
value="<%= data.hero?.title || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subtitle</label>
|
||||
<input type="text" class="form-control" id="heroSubtitle"
|
||||
name="heroSubtitle" value="<%= data.hero?.subtitle || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input type="text" class="form-control" id="heroHeading"
|
||||
name="heroHeading" value="<%= data.hero?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" id="heroDescription"
|
||||
name="heroDescription"
|
||||
rows="2"><%= data.hero?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Visa Options Tab -->
|
||||
<div class="tab-pane fade" id="visaOptions" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-medium mb-0">Visa Options</h6>
|
||||
<button type="button" class="btn btn-primary btn-sm"
|
||||
onclick="addVisaOption()">
|
||||
<i class="fas fa-plus"></i> Add Option
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-muted small">These options will appear in the visa type selection
|
||||
dropdown on the appointment form.</p>
|
||||
<div id="visaOptionsContainer">
|
||||
<% if (data.visaOptions && data.visaOptions.length> 0) { %>
|
||||
<% data.visaOptions.forEach((option, index)=> { %>
|
||||
<div class="input-group mb-2 visa-option-item">
|
||||
<span class="input-group-text"><i
|
||||
class="fas fa-passport"></i></span>
|
||||
<input type="text" class="form-control visa-option-input"
|
||||
value="<%= option %>" placeholder="Enter visa option">
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
onclick="removeVisaOption(this)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<% }); %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Tab -->
|
||||
<div class="tab-pane fade" id="form" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-body">
|
||||
<h6 class="fw-medium mb-3">Form Settings</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Form Heading</label>
|
||||
<input type="text" class="form-control" id="formHeading"
|
||||
value="<%= data.form?.heading || '' %>">
|
||||
</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 || 'Request Appointment' %>">
|
||||
</div>
|
||||
<!-- Hidden fields for submitButton icon and buttonClass -->
|
||||
<input type="hidden" id="formSubmitButtonIcon"
|
||||
value="<%= data.form?.submitButton?.icon || 'fa-solid fa-arrow-right' %>">
|
||||
<input type="hidden" id="formSubmitButtonClass"
|
||||
value="<%= data.form?.submitButton?.buttonClass || 'theme-btn' %>">
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-medium mb-0">Form Fields</h6>
|
||||
<button type="button" class="btn btn-primary btn-sm"
|
||||
onclick="addFormField()">
|
||||
<i class="fas fa-plus"></i> Add Field
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="formFieldsContainer">
|
||||
<% if (data.form?.fields && data.form.fields.length> 0) { %>
|
||||
<% data.form.fields.forEach((field, index)=> { %>
|
||||
<div class="card mb-3 form-field-item">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Field Name</label>
|
||||
<input type="text"
|
||||
class="form-control field-name-input"
|
||||
value="<%= field.name || '' %>"
|
||||
placeholder="e.g., name">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Label</label>
|
||||
<input type="text"
|
||||
class="form-control field-label-input"
|
||||
value="<%= field.label || '' %>"
|
||||
placeholder="e.g., Your Name">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Type</label>
|
||||
<select class="form-select field-type-select">
|
||||
<option value="text" <%=field.type==='text'
|
||||
? 'selected' : '' %>>Text</option>
|
||||
<option value="email" <%=field.type==='email'
|
||||
? 'selected' : '' %>>Email</option>
|
||||
<option value="tel" <%=field.type==='tel'
|
||||
? 'selected' : '' %>>Phone</option>
|
||||
<option value="textarea"
|
||||
<%=field.type==='textarea' ? 'selected' : ''
|
||||
%>>Textarea</option>
|
||||
<option value="date" <%=field.type==='date'
|
||||
? 'selected' : '' %>>Date</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Col Class</label>
|
||||
<select class="form-select field-col-select">
|
||||
<option value="col-lg-4"
|
||||
<%=field.colClass==='col-lg-4' ? 'selected'
|
||||
: '' %>>1/3 Width</option>
|
||||
<option value="col-lg-6"
|
||||
<%=field.colClass==='col-lg-6' ? 'selected'
|
||||
: '' %>>1/2 Width</option>
|
||||
<option value="col-lg-12"
|
||||
<%=field.colClass==='col-lg-12' ? 'selected'
|
||||
: '' %>>Full Width</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Required</label>
|
||||
<div class="form-check mt-2">
|
||||
<input
|
||||
class="form-check-input field-required-check"
|
||||
type="checkbox" <%=field.required
|
||||
? 'checked' : '' %>>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Placeholder</label>
|
||||
<input type="text"
|
||||
class="form-control field-placeholder-input"
|
||||
value="<%= field.placeholder || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger btn-sm mt-3"
|
||||
onclick="removeFormField(this)">
|
||||
<i class="fas fa-trash me-2"></i>Remove Field
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submissions Tab -->
|
||||
<div class="tab-pane fade" id="submissions" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-medium mb-0">Recent Submissions</h6>
|
||||
</div>
|
||||
|
||||
<!-- Date Filter -->
|
||||
<div class="row g-2 mb-4 align-items-end" id="filterContainer">
|
||||
<input type="hidden" id="filterTab" value="submissions">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small text-muted">Start Date</label>
|
||||
<input type="date" class="form-control form-control-sm"
|
||||
id="filterStartDate" value="<%= locals.startDate || '' %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small text-muted">End Date</label>
|
||||
<input type="date" class="form-control form-control-sm"
|
||||
id="filterEndDate" value="<%= locals.endDate || '' %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button type="button" class="btn btn-sm btn-primary w-100"
|
||||
onclick="applyDateFilter()">
|
||||
<i class="fas fa-filter me-1"></i> Filter
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<a href="/admin/appointment?tab=submissions"
|
||||
class="btn btn-sm btn-outline-secondary w-100">
|
||||
<i class="fas fa-times me-1"></i> Clear
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Name</th>
|
||||
<th>Contact</th>
|
||||
<th>Appt Date</th>
|
||||
<th>Visa Types</th>
|
||||
<th>Message</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (locals.submissions && submissions.length> 0) { %>
|
||||
<% submissions.forEach(submission=> { %>
|
||||
<tr>
|
||||
<td>
|
||||
<%= new
|
||||
Date(submission.createdAt).toLocaleDateString()
|
||||
%>
|
||||
<br>
|
||||
<small class="text-muted">
|
||||
<%= new
|
||||
Date(submission.createdAt).toLocaleTimeString([],
|
||||
{hour: '2-digit' , minute:'2-digit'}) %>
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<%= submission.name %>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column">
|
||||
<a href="mailto:<%= submission.email %>"
|
||||
class="text-decoration-none"><i
|
||||
class="fas fa-envelope me-1"></i>
|
||||
<%= submission.email %>
|
||||
</a>
|
||||
<% if(submission.phone) { %>
|
||||
<span class="text-muted small"><i
|
||||
class="fas fa-phone me-1"></i>
|
||||
<%= submission.phone %>
|
||||
</span>
|
||||
<% } %>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<%= submission.appointmentDate || '-' %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (submission.visaTypes &&
|
||||
submission.visaTypes.length> 0) { %>
|
||||
<% submission.visaTypes.forEach(type=> { %>
|
||||
<span
|
||||
class="badge bg-light text-dark border me-1">
|
||||
<%= type %>
|
||||
</span>
|
||||
<% }); %>
|
||||
<% } else { %>
|
||||
<span class="text-muted">-</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (submission.message) { %>
|
||||
<div title="<%= submission.message %>"
|
||||
style="max-width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<%= submission.message %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<span class="text-muted">-</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% let statusClass='bg-secondary' ;
|
||||
if(submission.status==='pending' )
|
||||
statusClass='bg-warning text-dark' ;
|
||||
if(submission.status==='confirmed' )
|
||||
statusClass='bg-success' ;
|
||||
if(submission.status==='completed' )
|
||||
statusClass='bg-info text-dark' ;
|
||||
if(submission.status==='cancelled' )
|
||||
statusClass='bg-danger' ; %>
|
||||
<span
|
||||
class="badge <%= statusClass %> rounded-pill">
|
||||
<%= submission.status %>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
onclick="openStatusModal('<%= submission._id %>', '<%= submission.status %>')"
|
||||
title="Update Status">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
<% } else { %>
|
||||
<tr>
|
||||
<td colspan="8"
|
||||
class="text-center py-4 text-muted">No
|
||||
submissions found</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-3 text-end">
|
||||
<small class="text-muted">Showing last 50 submissions</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fixed Bottom Buttons -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
|
||||
<i class="fas fa-undo me-2"></i>Reset
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<i class="fas fa-save me-2"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Update Modal -->
|
||||
<div class="modal fade" id="statusModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Update Status</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="statusForm">
|
||||
<input type="hidden" id="statusSubmissionId">
|
||||
<div class="mb-3">
|
||||
<label for="statusSelect" class="form-label">Status</label>
|
||||
<select class="form-select" id="statusSelect">
|
||||
<option value="pending">Pending</option>
|
||||
<option value="confirmed">Confirmed</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveStatus()">Save changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="application/json" id="appointmentDataJson"><%- JSON.stringify(data) %></script>
|
||||
|
||||
<script>
|
||||
let originalFormData = null;
|
||||
let statusModal = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
try {
|
||||
var jsonScript = document.getElementById('appointmentDataJson');
|
||||
originalFormData = JSON.parse(jsonScript.textContent);
|
||||
} catch (e) {
|
||||
console.error('Error parsing originalFormData:', e);
|
||||
originalFormData = {};
|
||||
}
|
||||
|
||||
// Check for tab parameter in URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const tab = urlParams.get('tab');
|
||||
if (tab) {
|
||||
const triggerEl = document.querySelector(`a[href="#${tab}"]`);
|
||||
if (triggerEl) {
|
||||
const tabInstance = new bootstrap.Tab(triggerEl);
|
||||
tabInstance.show();
|
||||
}
|
||||
}
|
||||
|
||||
// Move modal to body to prevent backdrop issues
|
||||
const statusModalEl = document.getElementById('statusModal');
|
||||
if (statusModalEl) {
|
||||
document.body.appendChild(statusModalEl);
|
||||
}
|
||||
statusModal = new bootstrap.Modal(statusModalEl);
|
||||
|
||||
updateAllJsonInputs();
|
||||
initializeFormHandlers();
|
||||
});
|
||||
|
||||
function applyDateFilter() {
|
||||
const startDate = document.getElementById('filterStartDate').value;
|
||||
const endDate = document.getElementById('filterEndDate').value;
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('tab', 'submissions');
|
||||
|
||||
if (startDate) {
|
||||
url.searchParams.set('startDate', startDate);
|
||||
} else {
|
||||
url.searchParams.delete('startDate');
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
url.searchParams.set('endDate', endDate);
|
||||
} else {
|
||||
url.searchParams.delete('endDate');
|
||||
}
|
||||
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
function openStatusModal(id, currentStatus) {
|
||||
document.getElementById('statusSubmissionId').value = id;
|
||||
document.getElementById('statusSelect').value = currentStatus;
|
||||
statusModal.show();
|
||||
}
|
||||
|
||||
async function saveStatus() {
|
||||
const id = document.getElementById('statusSubmissionId').value;
|
||||
const status = document.getElementById('statusSelect').value;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/appointments/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ status })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Determine CSS class for the notification or badge
|
||||
// Since this is generic, we'll reload or update UI manually if complex.
|
||||
// Reload is safest to show updated table state (including sorting/filtering if any)
|
||||
// But let's try to be smooth:
|
||||
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Failed to update status: ' + (result.error || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating status:', error);
|
||||
alert('Error updating status');
|
||||
}
|
||||
}
|
||||
|
||||
function initializeFormHandlers() {
|
||||
const form = document.getElementById('appointmentForm');
|
||||
form.addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
|
||||
|
||||
try {
|
||||
updateJsonData();
|
||||
this.submit();
|
||||
} catch (error) {
|
||||
console.error('Error updating data:', error);
|
||||
alert('Failed to process form data. Please try again.');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
|
||||
}
|
||||
});
|
||||
|
||||
// Image upload buttons
|
||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
const imageType = this.dataset.imageType;
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
});
|
||||
|
||||
// Update preview when background image changes
|
||||
document.getElementById('heroBackgroundImage').addEventListener('input', function () {
|
||||
updateHeroImagePreview(this.value);
|
||||
});
|
||||
}
|
||||
|
||||
function updateHeroImagePreview(imagePath) {
|
||||
const previewContainer = document.getElementById('heroImagePreview');
|
||||
if (imagePath) {
|
||||
let imgSrc = imagePath;
|
||||
if (!imgSrc.startsWith('http://') && !imgSrc.startsWith('https://')) {
|
||||
imgSrc = imgSrc.startsWith('/') ? imgSrc : '/' + imgSrc;
|
||||
}
|
||||
previewContainer.innerHTML = `
|
||||
<img src="${imgSrc}" class="img-thumbnail" id="heroPreviewImg"
|
||||
style="height: 200px; width: 100%; object-fit: cover;"
|
||||
alt="Background image preview"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
||||
<div class="border rounded p-5 text-center text-muted"
|
||||
style="height: 200px; display: none; align-items: center; justify-content: center;">
|
||||
Image preview
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
previewContainer.innerHTML = `
|
||||
<div class="border rounded p-5 text-center text-muted"
|
||||
style="height: 200px; display: flex; align-items: center; justify-content: center;">
|
||||
Image preview
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateAllJsonInputs() {
|
||||
updateJsonData();
|
||||
}
|
||||
|
||||
function updateJsonData() {
|
||||
// Hero data
|
||||
const heroData = {
|
||||
title: document.getElementById('heroTitle').value || '',
|
||||
backgroundImage: document.getElementById('heroBackgroundImage').value || '',
|
||||
subtitle: document.getElementById('heroSubtitle').value || '',
|
||||
heading: document.getElementById('heroHeading').value || '',
|
||||
description: document.getElementById('heroDescription').value || '',
|
||||
};
|
||||
document.getElementById('heroJson').value = JSON.stringify(heroData);
|
||||
|
||||
// Visa options
|
||||
const visaOptions = [];
|
||||
document.querySelectorAll('.visa-option-input').forEach(input => {
|
||||
if (input.value.trim()) {
|
||||
visaOptions.push(input.value.trim());
|
||||
}
|
||||
});
|
||||
document.getElementById('visaOptionsJson').value = JSON.stringify(visaOptions);
|
||||
|
||||
// Form data
|
||||
const fields = [];
|
||||
document.querySelectorAll('.form-field-item').forEach(item => {
|
||||
fields.push({
|
||||
name: item.querySelector('.field-name-input').value || '',
|
||||
label: item.querySelector('.field-label-input').value || '',
|
||||
type: item.querySelector('.field-type-select').value || 'text',
|
||||
placeholder: item.querySelector('.field-placeholder-input').value || '',
|
||||
required: item.querySelector('.field-required-check').checked,
|
||||
colClass: item.querySelector('.field-col-select').value || 'col-lg-12',
|
||||
});
|
||||
});
|
||||
|
||||
const formData = {
|
||||
heading: document.getElementById('formHeading').value || '',
|
||||
fields: fields,
|
||||
submitButton: {
|
||||
text: document.getElementById('formSubmitButtonText').value || 'Request Appointment',
|
||||
icon: document.getElementById('formSubmitButtonIcon').value || 'fa-solid fa-arrow-right',
|
||||
buttonClass: document.getElementById('formSubmitButtonClass').value || 'theme-btn',
|
||||
},
|
||||
};
|
||||
document.getElementById('formJson').value = JSON.stringify(formData);
|
||||
}
|
||||
|
||||
function addVisaOption() {
|
||||
const container = document.getElementById('visaOptionsContainer');
|
||||
const html = `
|
||||
<div class="input-group mb-2 visa-option-item">
|
||||
<span class="input-group-text"><i class="fas fa-passport"></i></span>
|
||||
<input type="text" class="form-control visa-option-input" value="" placeholder="Enter visa option">
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removeVisaOption(this)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function removeVisaOption(button) {
|
||||
button.closest('.visa-option-item').remove();
|
||||
}
|
||||
|
||||
function addFormField() {
|
||||
const container = document.getElementById('formFieldsContainer');
|
||||
const html = `
|
||||
<div class="card mb-3 form-field-item">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Field Name</label>
|
||||
<input type="text" class="form-control field-name-input" value="" placeholder="e.g., name">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Label</label>
|
||||
<input type="text" class="form-control field-label-input" value="" placeholder="e.g., Your Name">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Type</label>
|
||||
<select class="form-select field-type-select">
|
||||
<option value="text" selected>Text</option>
|
||||
<option value="email">Email</option>
|
||||
<option value="tel">Phone</option>
|
||||
<option value="textarea">Textarea</option>
|
||||
<option value="date">Date</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Col Class</label>
|
||||
<select class="form-select field-col-select">
|
||||
<option value="col-lg-4">1/3 Width</option>
|
||||
<option value="col-lg-6">1/2 Width</option>
|
||||
<option value="col-lg-12" selected>Full Width</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Required</label>
|
||||
<div class="form-check mt-2">
|
||||
<input class="form-check-input field-required-check" type="checkbox">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Placeholder</label>
|
||||
<input type="text" class="form-control field-placeholder-input" value="">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeFormField(this)">
|
||||
<i class="fas fa-trash me-2"></i>Remove Field
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function removeFormField(button) {
|
||||
button.closest('.form-field-item').remove();
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
if (confirm('Are you sure you want to reset all changes?')) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
// Image uploader function (reuse from shared)
|
||||
function openImageUploader(targetInput, imageType) {
|
||||
// Open upload modal or trigger file input
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/upload/image', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success && result.imagePath) {
|
||||
document.getElementById(targetInput).value = result.imagePath;
|
||||
if (targetInput === 'heroBackgroundImage') {
|
||||
updateHeroImagePreview(result.imagePath);
|
||||
}
|
||||
} else {
|
||||
alert('Upload failed: ' + (result.error || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
alert('Upload failed. Please try again.');
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
</script>
|
||||
@@ -1,699 +1,243 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
|
||||
<%= title %>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">System activity and change tracking</p>
|
||||
<!-- Page Header -->
|
||||
<div class="page-title-area">
|
||||
<div>
|
||||
<h1>Audit Logs</h1>
|
||||
<p class="subtitle">System activity and change tracking</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-warning" onclick="openCleanupModal()">
|
||||
<i class="fas fa-broom"></i> Cleanup Old Logs
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-body" style="padding:1rem 1.25rem;">
|
||||
<form method="GET" action="/admin/audit-logs">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Model</label>
|
||||
<select class="form-select" name="model">
|
||||
<option value="">All Models</option>
|
||||
<% uniqueModels.forEach(model => { %>
|
||||
<option value="<%= model %>" <%= query.model === model ? 'selected' : '' %>><%= model %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-warning" onclick="openCleanupModal()">
|
||||
<i class="fas fa-broom me-2"></i>Cleanup Old Logs
|
||||
</button>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Action</label>
|
||||
<select class="form-select" name="action">
|
||||
<option value="">All Actions</option>
|
||||
<% uniqueActions.forEach(action => { %>
|
||||
<option value="<%= action %>" <%= query.action === action ? 'selected' : '' %>><%= action %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<form method="GET" action="/admin/audit-logs" class="row g-3">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Model</label>
|
||||
<select class="form-select" name="model">
|
||||
<option value="">All Models</option>
|
||||
<% uniqueModels.forEach(model => { %>
|
||||
<option value="<%= model %>" <%= query.model === model ? 'selected' : '' %>>
|
||||
<%= model %>
|
||||
</option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Action</label>
|
||||
<select class="form-select" name="action">
|
||||
<option value="">All Actions</option>
|
||||
<% uniqueActions.forEach(action => { %>
|
||||
<option value="<%= action %>" <%= query.action === action ? 'selected' : '' %>>
|
||||
<%= action %>
|
||||
</option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">User</label>
|
||||
<select class="form-select" name="user">
|
||||
<option value="">All Users</option>
|
||||
<% users.forEach(user => { %>
|
||||
<option value="<%= user._id %>" <%= query.user === user._id.toString() ? 'selected' : '' %>>
|
||||
<%= user.username %>
|
||||
</option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">From Date</label>
|
||||
<input type="date" class="form-control" name="dateFrom" value="<%= query.dateFrom || '' %>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">To Date</label>
|
||||
<input type="date" class="form-control" name="dateTo" value="<%= query.dateTo || '' %>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Items per page</label>
|
||||
<select class="form-select" name="limit">
|
||||
<option value="5" <%= (query.limit && query.limit == '5') ? 'selected' : '' %>>5 items</option>
|
||||
<option value="7" <%= (!query.limit || query.limit == '7') ? 'selected' : '' %>>7 items</option>
|
||||
<option value="10" <%= (query.limit && query.limit == '10') ? 'selected' : '' %>>10 items</option>
|
||||
<option value="15" <%= (query.limit && query.limit == '15') ? 'selected' : '' %>>15 items</option>
|
||||
<option value="20" <%= (query.limit && query.limit == '20') ? 'selected' : '' %>>20 items</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label"> </label>
|
||||
<div class="d-flex gap-1">
|
||||
<button type="submit" class="btn flex-fill" style="background-color: var(--primary-color); color: white;">
|
||||
<i class="fas fa-search me-1"></i>Filter
|
||||
</button>
|
||||
<a href="/admin/audit-logs" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">User</label>
|
||||
<select class="form-select" name="user">
|
||||
<option value="">All Users</option>
|
||||
<% users.forEach(u => { %>
|
||||
<option value="<%= u._id %>" <%= query.user === u._id.toString() ? 'selected' : '' %>><%= u.username %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">From Date</label>
|
||||
<input type="date" class="form-control" name="dateFrom" value="<%= query.dateFrom || '' %>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">To Date</label>
|
||||
<input type="date" class="form-control" name="dateTo" value="<%= query.dateTo || '' %>">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label">Per page</label>
|
||||
<select class="form-select" name="limit" onchange="this.form.submit()">
|
||||
<option value="8" <%= (!query.limit || query.limit == '8') ? 'selected' : '' %>>8</option>
|
||||
<option value="15" <%= query.limit == '15' ? 'selected' : '' %>>15</option>
|
||||
<option value="25" <%= query.limit == '25' ? 'selected' : '' %>>25</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1 d-flex gap-1">
|
||||
<button type="submit" class="btn btn-primary flex-fill"><i class="fas fa-search"></i></button>
|
||||
<a href="/admin/audit-logs" class="btn btn-outline-secondary"><i class="fas fa-times"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audit Logs List -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<% if (auditLogs && auditLogs.length > 0) { %>
|
||||
<!-- Summary Stats -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted">
|
||||
Showing <%= ((pagination.current - 1) * pagination.limit) + 1 %> -
|
||||
<%= Math.min(pagination.current * pagination.limit, pagination.totalCount) %>
|
||||
of <%= pagination.totalCount %> audit logs
|
||||
<% if (pagination.totalCount > pagination.limit) { %>
|
||||
<span class="badge bg-info ms-2">
|
||||
<%= pagination.total %> pages
|
||||
</span>
|
||||
<% } %>
|
||||
</span>
|
||||
</div>
|
||||
<!-- Table -->
|
||||
<div class="card border-0">
|
||||
<div class="card-header">
|
||||
<h5 class="card-header-title"><i class="fas fa-shield-alt"></i> Activity Log</h5>
|
||||
<% if (pagination) { %>
|
||||
<span class="badge badge-soft-primary"><%= pagination.totalCount %> records</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<% if (auditLogs && auditLogs.length > 0) { %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:130px">Date / Time</th>
|
||||
<th>Model</th>
|
||||
<th>Action</th>
|
||||
<th>User</th>
|
||||
<th>Changes</th>
|
||||
<th>IP Address</th>
|
||||
<th style="width:80px">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% auditLogs.forEach(log => { %>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="font-size:0.8rem;font-weight:500"><%= new Date(log.createdAt).toLocaleDateString('en-GB') %></div>
|
||||
<div style="font-size:0.75rem;color:var(--text-muted)"><%= new Date(log.createdAt).toLocaleTimeString() %></div>
|
||||
</td>
|
||||
<td><span class="badge badge-soft-primary"><%= log.model %></span></td>
|
||||
<td>
|
||||
<%
|
||||
let badgeClass = 'badge-soft-primary';
|
||||
if (log.action.includes('CREATE')) badgeClass = 'bg-soft-success';
|
||||
else if (log.action.includes('UPDATE')) badgeClass = 'bg-soft-warning';
|
||||
else if (log.action.includes('DELETE')) badgeClass = 'bg-soft-danger';
|
||||
%>
|
||||
<span class="badge <%= badgeClass %>"><%= log.action %></span>
|
||||
</td>
|
||||
<td>
|
||||
<% if (log.performedBy) { %>
|
||||
<div style="font-weight:500;font-size:0.8125rem"><%= log.performedBy.username %></div>
|
||||
<div style="font-size:0.75rem;color:var(--text-muted)"><%= log.performedBy.email %></div>
|
||||
<% } else { %>
|
||||
<span style="color:var(--text-muted);font-size:0.8125rem">System</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (log.changes && log.changes.length > 0) { %>
|
||||
<span class="badge badge-soft-accent"><%= log.changes.length %> field<%= log.changes.length > 1 ? 's' : '' %></span>
|
||||
<div class="mt-1">
|
||||
<% log.changes.slice(0, 2).forEach(change => { %>
|
||||
<div style="font-size:0.75rem;color:var(--text-muted)"><%= change.field %></div>
|
||||
<% }); %>
|
||||
<% if (log.changes.length > 2) { %>
|
||||
<div style="font-size:0.75rem;color:var(--text-muted)">+<%= log.changes.length - 2 %> more</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="btn-group btn-group-sm me-3" role="group">
|
||||
<input type="radio" class="btn-check" name="viewMode" id="tableView" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-secondary" for="tableView">
|
||||
<i class="fas fa-table"></i> Table
|
||||
</label>
|
||||
<input type="radio" class="btn-check" name="viewMode" id="cardView" autocomplete="off">
|
||||
<label class="btn btn-outline-secondary" for="cardView">
|
||||
<i class="fas fa-th-large"></i> Cards
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<span style="color:var(--text-muted)">—</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td style="font-size:0.8rem;color:var(--text-muted)"><%= log.ipAddress %></td>
|
||||
<td>
|
||||
<a href="/admin/audit-logs/<%= log._id %>" class="btn btn-sm btn-outline-primary btn-icon" title="View">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Table View -->
|
||||
<div id="tableViewContent" class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width: 140px; color: var(--primary-dark);">Date/Time</th>
|
||||
<th scope="col" style="color: var(--primary-dark);">Model</th>
|
||||
<th scope="col" style="color: var(--primary-dark);">Action</th>
|
||||
<th scope="col" style="color: var(--primary-dark);">User</th>
|
||||
<th scope="col" style="color: var(--primary-dark);">Changes</th>
|
||||
<th scope="col" style="color: var(--primary-dark);">IP Address</th>
|
||||
<th scope="col" style="width: 120px; color: var(--primary-dark);">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% auditLogs.forEach((log, index) => { %>
|
||||
<tr>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
<%= new Date(log.createdAt).toLocaleDateString() %><br>
|
||||
<%= new Date(log.createdAt).toLocaleTimeString() %>
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" style="background-color: var(--primary-color); color: white;">
|
||||
<%= log.model %>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<%
|
||||
let actionStyle = 'background-color: var(--primary-color); color: white;';
|
||||
if (log.action.includes('CREATE')) actionStyle = 'background-color: #28a745; color: white;';
|
||||
else if (log.action.includes('UPDATE')) actionStyle = 'background-color: #ffc107; color: #212529;';
|
||||
else if (log.action.includes('DELETE')) actionStyle = 'background-color: #dc3545; color: white;';
|
||||
%>
|
||||
<span class="badge" style="<%= actionStyle %>">
|
||||
<%= log.action %>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<% if (log.performedBy) { %>
|
||||
<div>
|
||||
<strong style="color: var(--primary-dark);"><%= log.performedBy.username %></strong><br>
|
||||
<small class="text-muted"><%= log.performedBy.email %></small>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<span class="text-muted">System</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (log.changes && log.changes.length > 0) { %>
|
||||
<span class="badge" style="background-color: var(--primary-color); color: white;">
|
||||
<%= log.changes.length %> field<%= log.changes.length > 1 ? 's' : '' %>
|
||||
</span>
|
||||
<div class="mt-1">
|
||||
<% log.changes.slice(0, 3).forEach(change => { %>
|
||||
<small class="text-muted d-block">
|
||||
<strong><%= change.field %></strong>
|
||||
</small>
|
||||
<% }); %>
|
||||
<% if (log.changes.length > 3) { %>
|
||||
<small class="text-muted">
|
||||
+<%= log.changes.length - 3 %> more...
|
||||
</small>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<span class="text-muted">-</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
<%= log.ipAddress %>
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="/admin/audit-logs/<%= log._id %>" class="btn btn-sm" style="background-color: var(--primary-color); color: white;">
|
||||
<i class="fas fa-eye me-1"></i>View
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Card View (Hidden by default) -->
|
||||
<div id="cardViewContent" style="display: none;">
|
||||
<% auditLogs.forEach((log, index) => { %>
|
||||
<div class="audit-card">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center py-2">
|
||||
<span class="badge" style="background-color: var(--primary-color); color: white; font-size: 0.7rem;"><%= log.model %></span>
|
||||
<small class="text-muted" style="font-size: 0.7rem;">
|
||||
<%= new Date(log.createdAt).toLocaleDateString() %>
|
||||
</small>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<%
|
||||
let actionStyle = 'background-color: var(--primary-color); color: white;';
|
||||
if (log.action.includes('CREATE')) actionStyle = 'background-color: #28a745; color: white;';
|
||||
else if (log.action.includes('UPDATE')) actionStyle = 'background-color: #ffc107; color: #212529;';
|
||||
else if (log.action.includes('DELETE')) actionStyle = 'background-color: #dc3545; color: white;';
|
||||
%>
|
||||
<span class="badge" style="<%= actionStyle %>; font-size: 0.65rem;">
|
||||
<%= log.action %>
|
||||
</span>
|
||||
<small class="text-muted" style="font-size: 0.7rem;">
|
||||
<%= new Date(log.createdAt).toLocaleTimeString() %>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<strong style="font-size: 0.8rem;">User:</strong>
|
||||
<% if (log.performedBy) { %>
|
||||
<div style="color: var(--primary-dark); font-size: 0.8rem;"><%= log.performedBy.username %></div>
|
||||
<small class="text-muted" style="font-size: 0.7rem;"><%= log.performedBy.email %></small>
|
||||
<% } else { %>
|
||||
<span class="text-muted" style="font-size: 0.8rem;">System</span>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<% if (log.changes && log.changes.length > 0) { %>
|
||||
<div class="mb-2">
|
||||
<strong style="font-size: 0.8rem;">Changes:</strong>
|
||||
<span class="badge ms-1" style="background-color: var(--primary-color); color: white; font-size: 0.65rem;">
|
||||
<%= log.changes.length %> field<%= log.changes.length > 1 ? 's' : '' %>
|
||||
</span>
|
||||
<div class="mt-1">
|
||||
<% log.changes.slice(0, 2).forEach(change => { %>
|
||||
<small class="text-muted d-block" style="font-size: 0.7rem;">
|
||||
• <%= change.field %>
|
||||
</small>
|
||||
<% }); %>
|
||||
<% if (log.changes.length > 2) { %>
|
||||
<small class="text-muted" style="font-size: 0.7rem;">
|
||||
+<%= log.changes.length - 2 %> more...
|
||||
</small>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="mb-2">
|
||||
<small class="text-muted" style="font-size: 0.7rem;">
|
||||
IP: <%= log.ipAddress %>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer p-2">
|
||||
<a href="/admin/audit-logs/<%= log._id %>" class="btn btn-sm w-100" style="background-color: var(--primary-color); color: white; font-size: 0.8rem;">
|
||||
<i class="fas fa-eye me-1"></i>View Details
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<% if (pagination && pagination.total > 1) { %>
|
||||
<nav aria-label="Audit log pagination" class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
<% if (pagination.current > 1) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=<%= pagination.current - 1 %><%= Object.keys(query).map(key => key !== 'page' ? '&' + key + '=' + encodeURIComponent(query[key]) : '').join('') %>" style="color: var(--primary-dark);">Previous</a>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% for (let i = 1; i <= pagination.total; i++) { %>
|
||||
<% if (i === pagination.current) { %>
|
||||
<li class="page-item active">
|
||||
<span class="page-link" style="background-color: var(--primary-color); border-color: var(--primary-color);"><%= i %></span>
|
||||
</li>
|
||||
<% } else if (i === 1 || i === pagination.total || (i >= pagination.current - 2 && i <= pagination.current + 2)) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=<%= i %><%= Object.keys(query).map(key => key !== 'page' ? '&' + key + '=' + encodeURIComponent(query[key]) : '').join('') %>" style="color: var(--primary-dark);">
|
||||
<%= i %>
|
||||
</a>
|
||||
</li>
|
||||
<% } else if (i === pagination.current - 3 || i === pagination.current + 3) { %>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">...</span>
|
||||
</li>
|
||||
<% } %>
|
||||
<% } %>
|
||||
|
||||
<% if (pagination.current < pagination.total) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=<%= pagination.current + 1 %><%= Object.keys(query).map(key => key !== 'page' ? '&' + key + '=' + encodeURIComponent(query[key]) : '').join('') %>" style="color: var(--primary-dark);">Next</a>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
|
||||
<!-- Pagination Info -->
|
||||
<div class="text-center mt-2">
|
||||
<small class="text-muted">
|
||||
Page <%= pagination.current %> of <%= pagination.total %>
|
||||
(showing <%= ((pagination.current - 1) * pagination.limit) + 1 %> -
|
||||
<%= Math.min(pagination.current * pagination.limit, pagination.totalCount) %>
|
||||
of <%= pagination.totalCount %> total items)
|
||||
</small>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Pagination -->
|
||||
<% if (pagination && pagination.total > 1) { %>
|
||||
<div class="d-flex align-items-center justify-content-between px-3 py-2" style="border-top:1px solid var(--border-light);">
|
||||
<small style="color:var(--text-muted)">
|
||||
Showing <%= ((pagination.current - 1) * pagination.limit) + 1 %>–<%= Math.min(pagination.current * pagination.limit, pagination.totalCount) %> of <%= pagination.totalCount %>
|
||||
</small>
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm mb-0" style="gap:2px;">
|
||||
<% if (pagination.current > 1) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=<%= pagination.current - 1 %><%= Object.keys(query).map(k => k !== 'page' ? '&' + k + '=' + encodeURIComponent(query[k]) : '').join('') %>">
|
||||
<i class="fas fa-chevron-left" style="font-size:0.7rem"></i>
|
||||
</a>
|
||||
</li>
|
||||
<% } %>
|
||||
<% for (let i = 1; i <= pagination.total; i++) { %>
|
||||
<% if (i === pagination.current) { %>
|
||||
<li class="page-item active">
|
||||
<span class="page-link" style="background:var(--primary-color);border-color:var(--primary-color)"><%= i %></span>
|
||||
</li>
|
||||
<% } else if (i === 1 || i === pagination.total || (i >= pagination.current - 2 && i <= pagination.current + 2)) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=<%= i %><%= Object.keys(query).map(k => k !== 'page' ? '&' + k + '=' + encodeURIComponent(query[k]) : '').join('') %>"><%= i %></a>
|
||||
</li>
|
||||
<% } else if (i === pagination.current - 3 || i === pagination.current + 3) { %>
|
||||
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||||
<% } %>
|
||||
<% } else { %>
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-clipboard-list text-muted mb-3" style="font-size: 3rem;"></i>
|
||||
<h5 class="text-muted mb-3">No Audit Logs Found</h5>
|
||||
<p class="text-muted">No audit logs match your current filters.</p>
|
||||
<a href="/admin/audit-logs" class="btn" style="background-color: var(--primary-color); color: white;">
|
||||
<i class="fas fa-refresh me-1"></i>Clear Filters
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
<% if (pagination.current < pagination.total) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=<%= pagination.current + 1 %><%= Object.keys(query).map(k => k !== 'page' ? '&' + k + '=' + encodeURIComponent(query[k]) : '').join('') %>">
|
||||
<i class="fas fa-chevron-right" style="font-size:0.7rem"></i>
|
||||
</a>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% } else { %>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon"><i class="fas fa-shield-alt"></i></div>
|
||||
<h5>No audit logs found</h5>
|
||||
<p>No activity matches your current filters.</p>
|
||||
<a href="/admin/audit-logs" class="btn btn-outline-primary">Clear Filters</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Cleanup Modal -->
|
||||
<div id="cleanupModal" class="custom-modal" style="display: none;">
|
||||
<div class="custom-modal-overlay"></div>
|
||||
<div class="custom-modal-content">
|
||||
<div class="custom-modal-header">
|
||||
<h5 class="custom-modal-title">
|
||||
<i class="fas fa-broom me-2"></i>Cleanup Old Audit Logs
|
||||
</h5>
|
||||
<button type="button" class="custom-modal-close" onclick="closeCleanupModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<form method="POST" action="/admin/audit-logs/cleanup">
|
||||
<div class="custom-modal-body">
|
||||
<p>Delete audit logs older than the specified number of days.</p>
|
||||
<div class="mb-3">
|
||||
<label for="days" class="form-label">Keep logs for (days):</label>
|
||||
<input type="number" class="form-control" id="days" name="days" value="90" min="1" max="365" required>
|
||||
<div class="form-text">Recommended: 90 days for compliance</div>
|
||||
</div>
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<strong>Warning:</strong> This action cannot be undone. Deleted audit logs will be permanently removed.
|
||||
</div>
|
||||
</div>
|
||||
<div class="custom-modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeCleanupModal()">Cancel</button>
|
||||
<button type="submit" class="btn btn-warning">
|
||||
<i class="fas fa-broom me-1"></i>Cleanup Logs
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<!-- Cleanup Modal -->
|
||||
<div id="cleanupModal" style="display:none;position:fixed;inset:0;z-index:1050;">
|
||||
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.45);" onclick="closeCleanupModal()"></div>
|
||||
<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;border-radius:var(--border-radius-lg);box-shadow:var(--shadow-lg);width:90%;max-width:460px;overflow:hidden;">
|
||||
<div style="padding:1rem 1.25rem;background:var(--warning-soft);border-bottom:1px solid var(--border-color);display:flex;align-items:center;justify-content:space-between;">
|
||||
<span style="font-weight:600;color:var(--warning-color);display:flex;align-items:center;gap:0.5rem;">
|
||||
<i class="fas fa-broom"></i> Cleanup Old Audit Logs
|
||||
</span>
|
||||
<button onclick="closeCleanupModal()" style="background:none;border:none;cursor:pointer;color:var(--warning-color);font-size:1.1rem;"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<form method="POST" action="/admin/audit-logs/cleanup">
|
||||
<div style="padding:1.25rem;">
|
||||
<p style="font-size:var(--font-size-sm);color:var(--text-muted);margin-bottom:1rem;">Delete audit logs older than the specified number of days.</p>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Keep logs for (days)</label>
|
||||
<input type="number" class="form-control" name="days" value="90" min="1" max="365" required>
|
||||
<div class="form-text">Recommended: 90 days for compliance</div>
|
||||
</div>
|
||||
<div class="alert d-flex gap-2 align-items-start" style="background:var(--warning-soft);border:1px solid rgba(217,119,6,0.2);border-radius:var(--border-radius-sm);padding:0.75rem;">
|
||||
<i class="fas fa-exclamation-triangle" style="color:var(--warning-color);margin-top:2px;flex-shrink:0;"></i>
|
||||
<span style="font-size:var(--font-size-sm);color:var(--warning-color);">This action cannot be undone. Deleted logs will be permanently removed.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:0.875rem 1.25rem;border-top:1px solid var(--border-color);display:flex;gap:0.5rem;justify-content:flex-end;background:#fafbfc;">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="closeCleanupModal()">Cancel</button>
|
||||
<button type="submit" class="btn btn-warning"><i class="fas fa-broom"></i> Cleanup</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// View mode toggle
|
||||
const tableViewRadio = document.getElementById('tableView');
|
||||
const cardViewRadio = document.getElementById('cardView');
|
||||
const tableViewContent = document.getElementById('tableViewContent');
|
||||
const cardViewContent = document.getElementById('cardViewContent');
|
||||
|
||||
// Debug: Check if elements exist
|
||||
console.log('Elements found:', {
|
||||
tableViewRadio: !!tableViewRadio,
|
||||
cardViewRadio: !!cardViewRadio,
|
||||
tableViewContent: !!tableViewContent,
|
||||
cardViewContent: !!cardViewContent
|
||||
});
|
||||
|
||||
if (!tableViewRadio || !cardViewRadio || !tableViewContent || !cardViewContent) {
|
||||
console.error('Some view toggle elements are missing!');
|
||||
return;
|
||||
}
|
||||
|
||||
tableViewRadio.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
tableViewContent.classList.remove('hidden');
|
||||
cardViewContent.classList.remove('active');
|
||||
console.log('Table view activated');
|
||||
}
|
||||
});
|
||||
|
||||
cardViewRadio.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
tableViewContent.classList.add('hidden');
|
||||
cardViewContent.classList.add('active');
|
||||
console.log('Card view activated with CSS Grid');
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-submit form when changing items per page
|
||||
const limitSelect = document.querySelector('select[name="limit"]');
|
||||
if (limitSelect) {
|
||||
limitSelect.addEventListener('change', function() {
|
||||
// Reset to page 1 when changing limit
|
||||
const pageInput = document.querySelector('input[name="page"]');
|
||||
if (pageInput) {
|
||||
pageInput.value = 1;
|
||||
} else {
|
||||
// Create hidden input for page
|
||||
const hiddenPageInput = document.createElement('input');
|
||||
hiddenPageInput.type = 'hidden';
|
||||
hiddenPageInput.name = 'page';
|
||||
hiddenPageInput.value = 1;
|
||||
this.form.appendChild(hiddenPageInput);
|
||||
}
|
||||
|
||||
// Submit the form
|
||||
this.form.submit();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function openCleanupModal() {
|
||||
document.getElementById('cleanupModal').style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeCleanupModal() {
|
||||
document.getElementById('cleanupModal').style.display = 'none';
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
|
||||
// Close modal when clicking overlay
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('custom-modal-overlay')) {
|
||||
closeCleanupModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal with Escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeCleanupModal();
|
||||
}
|
||||
});
|
||||
|
||||
function exportLogs() {
|
||||
// Get current filter parameters
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set('export', 'csv');
|
||||
|
||||
// Create download link
|
||||
const exportUrl = '/admin/audit-logs/export?' + params.toString();
|
||||
window.open(exportUrl, '_blank');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.table th {
|
||||
border-top: none;
|
||||
font-weight: 600;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.btn-group-sm .btn {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.pagination .page-link {
|
||||
color: var(--primary-dark) !important;
|
||||
background-color: #fff !important;
|
||||
border: 1px solid #dee2e6 !important;
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
color: var(--primary-color);
|
||||
border-color: var(--border-color);
|
||||
border-radius: var(--border-radius-sm) !important;
|
||||
font-size: 0.8125rem;
|
||||
padding: 0.3rem 0.6rem;
|
||||
}
|
||||
.pagination .page-link:hover { background: var(--primary-soft); border-color: var(--primary-color); }
|
||||
.pagination .page-item.active .page-link { color: #fff; }
|
||||
.pagination .page-item.disabled .page-link { color: var(--text-muted); }
|
||||
</style>
|
||||
|
||||
.pagination .page-item.active .page-link {
|
||||
background-color: var(--primary-color) !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
color: #fff !important;
|
||||
z-index: 3;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pagination .page-link:hover {
|
||||
color: var(--primary-color) !important;
|
||||
background-color: #f8f9fa !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.pagination .page-link:focus {
|
||||
color: var(--primary-color) !important;
|
||||
background-color: #f8f9fa !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
box-shadow: 0 0 0 0.2rem rgba(var(--primary-color-rgb), 0.25);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.pagination .page-item.disabled .page-link {
|
||||
color: #6c757d !important;
|
||||
background-color: #fff !important;
|
||||
border-color: #dee2e6 !important;
|
||||
}
|
||||
|
||||
/* Ensure pagination text is always visible */
|
||||
.pagination .page-link span,
|
||||
.pagination .page-link {
|
||||
display: inline-block;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
/* Custom Modal Styles */
|
||||
.custom-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1050;
|
||||
}
|
||||
|
||||
.custom-modal-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.custom-modal-content {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.custom-modal-header {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
background-color: #fff3cd;
|
||||
border-radius: 8px 8px 0 0;
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.custom-modal-title {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #856404;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.custom-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.2rem;
|
||||
color: #856404;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.custom-modal-close:hover {
|
||||
background-color: rgba(133, 100, 4, 0.1);
|
||||
}
|
||||
|
||||
.custom-modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.custom-modal-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid #dee2e6;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0 0 8px 8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Card View Improvements - Using CSS Grid for better control */
|
||||
#cardViewContent {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#cardViewContent.active {
|
||||
display: grid !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
#cardViewContent .card {
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
width: 100% !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
#cardViewContent .card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
#cardViewContent .card-header {
|
||||
background-color: #f8f9fa !important;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
#cardViewContent .card-footer {
|
||||
background-color: #f8f9fa !important;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
/* Table View */
|
||||
#tableViewContent {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#tableViewContent.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Responsive grid adjustments */
|
||||
@media (max-width: 1400px) {
|
||||
#cardViewContent {
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
#cardViewContent {
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
#cardViewContent {
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#cardViewContent {
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 1rem !important;
|
||||
}
|
||||
|
||||
.custom-modal-content {
|
||||
width: 95%;
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function openCleanupModal() { document.getElementById('cleanupModal').style.display = 'block'; }
|
||||
function closeCleanupModal() { document.getElementById('cleanupModal').style.display = 'none'; }
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeCleanupModal(); });
|
||||
</script>
|
||||
|
||||
@@ -1,314 +1,161 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
|
||||
<%= title %>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Detailed audit log information</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="/admin/audit-logs" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-2"></i>Back to Audit Logs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Main Information -->
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0" style="color: var(--primary-dark);">
|
||||
<i class="fas fa-info-circle me-2"></i>Audit Information
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Model:</label>
|
||||
<span class="badge ms-2" style="background-color: var(--primary-color); color: white;"><%= auditLog.model %></span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Action:</label>
|
||||
<%
|
||||
let actionStyle = 'background-color: var(--primary-color); color: white;';
|
||||
if (auditLog.action.includes('CREATE')) actionStyle = 'background-color: #28a745; color: white;';
|
||||
else if (auditLog.action.includes('UPDATE')) actionStyle = 'background-color: #ffc107; color: #212529;';
|
||||
else if (auditLog.action.includes('DELETE')) actionStyle = 'background-color: #dc3545; color: white;';
|
||||
%>
|
||||
<span class="badge ms-2" style="<%= actionStyle %>"><%= auditLog.action %></span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Document ID:</label>
|
||||
<code class="ms-2" style="background-color: #f8f9fa; color: var(--primary-dark);"><%= auditLog.documentId %></code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Date & Time:</label>
|
||||
<div class="ms-2">
|
||||
<%= new Date(auditLog.createdAt).toLocaleDateString() %>
|
||||
<%= new Date(auditLog.createdAt).toLocaleTimeString() %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">User:</label>
|
||||
<div class="ms-2">
|
||||
<% if (auditLog.performedBy) { %>
|
||||
<strong style="color: var(--primary-dark);"><%= auditLog.performedBy.username %></strong><br>
|
||||
<small class="text-muted"><%= auditLog.performedBy.email %></small>
|
||||
<% } else { %>
|
||||
<span class="text-muted">System</span>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">IP Address:</label>
|
||||
<code class="ms-2" style="background-color: #f8f9fa; color: var(--primary-dark);"><%= auditLog.ipAddress %></code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if (auditLog.userAgent) { %>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">User Agent:</label>
|
||||
<div class="ms-2">
|
||||
<small class="text-muted font-monospace"><%= auditLog.userAgent %></small>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Changes Details -->
|
||||
<% if (auditLog.changes && auditLog.changes.length > 0) { %>
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0" style="color: var(--primary-dark);">
|
||||
<i class="fas fa-edit me-2"></i>Field Changes
|
||||
<span class="badge ms-2" style="background-color: var(--primary-color); color: white;"><%= auditLog.changes.length %></span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if (user && (user.role === 'admin' || user.role === 'superadmin')) { %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 200px; color: var(--primary-dark);">Field</th>
|
||||
<th style="color: var(--primary-dark);">Before</th>
|
||||
<th style="color: var(--primary-dark);">After</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% auditLog.changes.forEach((change, index) => { %>
|
||||
<tr>
|
||||
<td>
|
||||
<strong style="color: var(--primary-dark);"><%= change.field %></strong>
|
||||
</td>
|
||||
<td>
|
||||
<div class="change-value before">
|
||||
<% if (change.before === null || change.before === undefined) { %>
|
||||
<span class="text-muted fst-italic">null</span>
|
||||
<% } else if (typeof change.before === 'object') { %>
|
||||
<pre class="mb-0"><%= JSON.stringify(change.before, null, 2) %></pre>
|
||||
<% } else { %>
|
||||
<%= change.before %>
|
||||
<% } %>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="change-value after">
|
||||
<% if (change.after === null || change.after === undefined) { %>
|
||||
<span class="text-muted fst-italic">null</span>
|
||||
<% } else if (typeof change.after === 'object') { %>
|
||||
<pre class="mb-0"><%= JSON.stringify(change.after, null, 2) %></pre>
|
||||
<% } else { %>
|
||||
<%= change.after %>
|
||||
<% } %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>Summary View:</strong> Detailed field values are restricted to administrators.
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="color: var(--primary-dark);">Field</th>
|
||||
<th style="color: var(--primary-dark);">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% auditLog.changes.forEach((change, index) => { %>
|
||||
<tr>
|
||||
<td>
|
||||
<strong style="color: var(--primary-dark);"><%= change.field %></strong>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info">Modified</span>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Summary Stats -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header">
|
||||
<h6 class="card-title mb-0" style="color: var(--primary-dark);">
|
||||
<i class="fas fa-chart-bar me-2"></i>Summary
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-6">
|
||||
<div class="border-end">
|
||||
<div class="h5 mb-0" style="color: var(--primary-color);">
|
||||
<%= auditLog.changes ? auditLog.changes.length : 0 %>
|
||||
</div>
|
||||
<small class="text-muted">Fields Changed</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="h5 mb-0" style="color: var(--primary-color);">
|
||||
<%= new Date(auditLog.createdAt).toLocaleDateString() === new Date().toLocaleDateString() ? 'Today' : 'Past' %>
|
||||
</div>
|
||||
<small class="text-muted">Timing</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Raw Data (Admin Only - Collapsible) -->
|
||||
<!-- <% if (user && (user.role === 'admin' || user.role === 'superadmin')) { %>
|
||||
<div class="card border-0 shadow-sm mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<button class="btn btn-link p-0 text-decoration-none" type="button" data-bs-toggle="collapse" data-bs-target="#rawDataCollapse" aria-expanded="false" style="color: var(--primary-dark);">
|
||||
<i class="fas fa-code me-2"></i>Raw Data <span class="badge bg-warning text-dark ms-2">Admin Only</span>
|
||||
<i class="fas fa-chevron-down ms-2"></i>
|
||||
</button>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="collapse" id="rawDataCollapse">
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>Security Notice:</strong> Raw data contains sensitive information and is only visible to administrators.
|
||||
</div>
|
||||
<div class="row">
|
||||
<% if (auditLog.before) { %>
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-danger">Before:</h6>
|
||||
<pre class="p-3 rounded" style="background-color: #f8f9fa; border: 1px solid #dee2e6; max-height: 400px; overflow-y: auto;"><%= JSON.stringify(auditLog.before, null, 2) %></pre>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if (auditLog.after) { %>
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-success">After:</h6>
|
||||
<pre class="p-3 rounded" style="background-color: #f8f9fa; border: 1px solid #dee2e6; max-height: 400px; overflow-y: auto;"><%= JSON.stringify(auditLog.after, null, 2) %></pre>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<% if (!auditLog.before && !auditLog.after) { %>
|
||||
<div class="text-center text-muted py-3">
|
||||
<i class="fas fa-info-circle me-2"></i>No raw data available for this audit log.
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="card border-0 shadow-sm mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0" style="color: var(--primary-dark);">
|
||||
<i class="fas fa-lock me-2"></i>Raw Data
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-shield-alt me-2"></i>
|
||||
<strong>Access Restricted:</strong> Raw data access is limited to administrators for security reasons.
|
||||
</div>
|
||||
<div class="text-center text-muted py-3">
|
||||
<i class="fas fa-user-shield" style="font-size: 3rem; opacity: 0.3;"></i>
|
||||
<p class="mt-3">Contact your administrator if you need access to detailed raw data.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %> -->
|
||||
<!-- Page Header -->
|
||||
<div class="page-title-area">
|
||||
<div>
|
||||
<h1>Audit Log Details</h1>
|
||||
<p class="subtitle">Detailed activity record</p>
|
||||
</div>
|
||||
<a href="/admin/audit-logs" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.change-value {
|
||||
max-width: 300px;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
<div class="row g-3">
|
||||
|
||||
.change-value pre {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.8rem;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
<!-- Main Info -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="card-header-title"><i class="fas fa-info-circle"></i> Audit Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<div class="form-label">Model</div>
|
||||
<span class="badge badge-soft-primary" style="font-size:0.8rem"><%= auditLog.model %></span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-label">Action</div>
|
||||
<%
|
||||
let badgeClass = 'badge-soft-primary';
|
||||
if (auditLog.action.includes('CREATE')) badgeClass = 'bg-soft-success';
|
||||
else if (auditLog.action.includes('UPDATE')) badgeClass = 'bg-soft-warning';
|
||||
else if (auditLog.action.includes('DELETE')) badgeClass = 'bg-soft-danger';
|
||||
%>
|
||||
<span class="badge <%= badgeClass %>" style="font-size:0.8rem"><%= auditLog.action %></span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-label">Document ID</div>
|
||||
<code style="font-size:0.8rem;color:var(--primary-color)"><%= auditLog.documentId %></code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<div class="form-label">Date & Time</div>
|
||||
<div style="font-size:var(--font-size-sm)">
|
||||
<%= new Date(auditLog.createdAt).toLocaleDateString('en-GB') %>
|
||||
<span style="color:var(--text-muted)"><%= new Date(auditLog.createdAt).toLocaleTimeString() %></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-label">Performed By</div>
|
||||
<% if (auditLog.performedBy) { %>
|
||||
<div style="font-weight:500;font-size:var(--font-size-sm)"><%= auditLog.performedBy.username %></div>
|
||||
<div style="font-size:var(--font-size-xs);color:var(--text-muted)"><%= auditLog.performedBy.email %></div>
|
||||
<% } else { %>
|
||||
<span style="color:var(--text-muted);font-size:var(--font-size-sm)">System</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-label">IP Address</div>
|
||||
<code style="font-size:0.8rem;color:var(--text-muted)"><%= auditLog.ipAddress %></code>
|
||||
</div>
|
||||
</div>
|
||||
<% if (auditLog.userAgent) { %>
|
||||
<div class="col-12">
|
||||
<div class="form-label">User Agent</div>
|
||||
<div style="font-size:0.75rem;color:var(--text-muted);font-family:monospace;word-break:break-all"><%= auditLog.userAgent %></div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
.change-value.before {
|
||||
background-color: #fff5f5;
|
||||
border-left: 3px solid #dc3545;
|
||||
padding-left: 8px;
|
||||
}
|
||||
<!-- Field Changes -->
|
||||
<% if (auditLog.changes && auditLog.changes.length > 0) { %>
|
||||
<div class="card border-0">
|
||||
<div class="card-header">
|
||||
<h5 class="card-header-title"><i class="fas fa-pen"></i> Field Changes</h5>
|
||||
<span class="badge badge-soft-accent"><%= auditLog.changes.length %> field<%= auditLog.changes.length > 1 ? 's' : '' %></span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<% if (user && (user.role === 'admin' || user.role === 'superadmin')) { %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:160px">Field</th>
|
||||
<th>Before</th>
|
||||
<th>After</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% auditLog.changes.forEach(change => { %>
|
||||
<tr>
|
||||
<td style="font-weight:500;font-size:var(--font-size-sm)"><%= change.field %></td>
|
||||
<td>
|
||||
<div style="background:var(--danger-soft);border-left:3px solid var(--danger-color);padding:0.4rem 0.6rem;border-radius:0 var(--border-radius-sm) var(--border-radius-sm) 0;font-size:0.8rem;word-break:break-all;">
|
||||
<% if (change.before === null || change.before === undefined) { %>
|
||||
<em style="color:var(--text-muted)">null</em>
|
||||
<% } else if (typeof change.before === 'object') { %>
|
||||
<pre style="margin:0;font-size:0.75rem;max-height:100px;overflow:auto"><%= JSON.stringify(change.before, null, 2) %></pre>
|
||||
<% } else { %>
|
||||
<%= change.before %>
|
||||
<% } %>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="background:var(--success-soft);border-left:3px solid var(--success-color);padding:0.4rem 0.6rem;border-radius:0 var(--border-radius-sm) var(--border-radius-sm) 0;font-size:0.8rem;word-break:break-all;">
|
||||
<% if (change.after === null || change.after === undefined) { %>
|
||||
<em style="color:var(--text-muted)">null</em>
|
||||
<% } else if (typeof change.after === 'object') { %>
|
||||
<pre style="margin:0;font-size:0.75rem;max-height:100px;overflow:auto"><%= JSON.stringify(change.after, null, 2) %></pre>
|
||||
<% } else { %>
|
||||
<%= change.after %>
|
||||
<% } %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="p-3">
|
||||
<div class="alert d-flex gap-2 align-items-center" style="background:var(--info-soft);border:1px solid rgba(14,116,144,0.15);border-radius:var(--border-radius-sm);padding:0.75rem;">
|
||||
<i class="fas fa-info-circle" style="color:var(--info-color)"></i>
|
||||
<span style="font-size:var(--font-size-sm)">Detailed field values are restricted to administrators.</span>
|
||||
</div>
|
||||
<% auditLog.changes.forEach(change => { %>
|
||||
<div style="display:flex;align-items:center;gap:0.5rem;padding:0.4rem 0;border-bottom:1px solid var(--border-light);">
|
||||
<span style="font-weight:500;font-size:var(--font-size-sm)"><%= change.field %></span>
|
||||
<span class="badge bg-soft-info">Modified</span>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
.change-value.after {
|
||||
background-color: #f0fff4;
|
||||
border-left: 3px solid #28a745;
|
||||
padding-left: 8px;
|
||||
}
|
||||
<!-- Sidebar summary -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0">
|
||||
<div class="card-header">
|
||||
<h5 class="card-header-title"><i class="fas fa-chart-bar"></i> Summary</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column gap-2">
|
||||
<div class="stat-card primary">
|
||||
<div class="stat-card-value"><%= auditLog.changes ? auditLog.changes.length : 0 %></div>
|
||||
<div class="stat-card-label">Fields Changed</div>
|
||||
</div>
|
||||
<div class="stat-card accent">
|
||||
<div class="stat-card-value" style="font-size:1.25rem"><%= new Date(auditLog.createdAt).toLocaleDateString('en-GB') === new Date().toLocaleDateString('en-GB') ? 'Today' : 'Past' %></div>
|
||||
<div class="stat-card-label">Timing</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
.font-monospace {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
color: var(--primary-dark) !important;
|
||||
}
|
||||
|
||||
.btn-link:hover {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.table th {
|
||||
border-top: none;
|
||||
font-weight: 600;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f8f9fa !important;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,300 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
|
||||
<%= title %>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Manage blog posts and articles</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<% if (typeof frontendUrl !=='undefined' ) { %>
|
||||
<a href="<%= frontendUrl %>/blog" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-2"></i>View Blog Page
|
||||
</a>
|
||||
<% } %>
|
||||
<a href="/admin/blog/create" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-2"></i>Create New Post
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<form method="GET" action="/admin/blog" class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Search</label>
|
||||
<input type="text" class="form-control" name="search" value="<%= query.search || '' %>"
|
||||
placeholder="Search title or excerpt...">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="">All</option>
|
||||
<option value="published" <%=query.status==='published' ? 'selected' : '' %>>Published</option>
|
||||
<option value="draft" <%=query.status==='draft' ? 'selected' : '' %>>Draft</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Category</label>
|
||||
<select class="form-select" name="category">
|
||||
<option value="">All Categories</option>
|
||||
<% categories.forEach(cat=> { %>
|
||||
<option value="<%= cat.name %>" <%=query.category===cat.name ? 'selected' : '' %>>
|
||||
<%= cat.name %>
|
||||
</option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label"> </label>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="fas fa-search me-1"></i>Filter
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label"> </label>
|
||||
<a href="/admin/blog" class="btn btn-outline-secondary w-100">
|
||||
<i class="fas fa-times me-1"></i>Clear
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blog List -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<% if (blogs && blogs.length> 0) { %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width: 60px">Image</th>
|
||||
<th scope="col">Title</th>
|
||||
<th scope="col">Category</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Author</th>
|
||||
<th scope="col">Published</th>
|
||||
<th scope="col" style="width: 200px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% blogs.forEach((blog, index)=> { %>
|
||||
<tr>
|
||||
<td>
|
||||
<% if (blog.featuredImage) { %>
|
||||
<img src="<%= getFullImageUrl(blog.featuredImage, backendUrl) %>"
|
||||
alt="<%= blog.title %>" class="img-thumbnail"
|
||||
style="width: 50px; height: 50px; object-fit: cover;">
|
||||
<% } else { %>
|
||||
<div class="bg-light d-flex align-items-center justify-content-center"
|
||||
style="width: 50px; height: 50px;">
|
||||
<i class="fas fa-image text-muted"></i>
|
||||
</div>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<span class="fw-medium">
|
||||
<%= blog.title %>
|
||||
</span>
|
||||
<% if (blog.isFeatured) { %>
|
||||
<span class="badge bg-warning text-dark ms-2">Featured</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
<%= blog.excerpt.substring(0, 60) %>...
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<% if (blog.category && blog.category.length> 0) { %>
|
||||
<% blog.category.slice(0, 2).forEach(cat=> { %>
|
||||
<span class="badge bg-secondary me-1">
|
||||
<%= cat %>
|
||||
</span>
|
||||
<% }); %>
|
||||
<% if (blog.category.length> 2) { %>
|
||||
<span class="text-muted">+<%= blog.category.length - 2 %></span>
|
||||
<% } %>
|
||||
<% } else { %>
|
||||
<span class="text-muted">-</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (blog.status==='published' ) { %>
|
||||
<span class="badge bg-success">Published</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-secondary">Draft</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<%= blog.author || 'Admin' %>
|
||||
</td>
|
||||
<td>
|
||||
<%= blog.publishedAt || '-' %>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<% if (typeof frontendUrl !=='undefined' ) { %>
|
||||
<a href="<%= frontendUrl %>/blog/<%= blog.slug %>" target="_blank"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
<% } %>
|
||||
<a href="/admin/blog/<%= blog._id %>/edit"
|
||||
class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-edit me-1"></i>Edit
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||
data-custom-modal="open" data-id="<%= blog._id %>"
|
||||
data-title="<%= blog.title %>">
|
||||
<i class="fas fa-trash-alt me-1"></i>Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<% if (pagination && pagination.total> 1) { %>
|
||||
<nav aria-label="Blog pagination" class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
<% if (pagination.current> 1) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link"
|
||||
href="?page=<%= pagination.current - 1 %><%= query.search ? '&search=' + query.search : '' %><%= query.status ? '&status=' + query.status : '' %><%= query.category ? '&category=' + query.category : '' %>">Previous</a>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% for (let i=1; i <=pagination.total; i++) { %>
|
||||
<% if (i===pagination.current) { %>
|
||||
<li class="page-item active">
|
||||
<span class="page-link">
|
||||
<%= i %>
|
||||
</span>
|
||||
</li>
|
||||
<% } else if (i===1 || i===pagination.total || (i>= pagination.current - 2
|
||||
&& i <= pagination.current + 2)) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link"
|
||||
href="?page=<%= i %><%= query.search ? '&search=' + query.search : '' %><%= query.status ? '&status=' + query.status : '' %><%= query.category ? '&category=' + query.category : '' %>">
|
||||
<%= i %>
|
||||
</a>
|
||||
</li>
|
||||
<% } else if (i===pagination.current - 3 || i===pagination.current +
|
||||
3) { %>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">...</span>
|
||||
</li>
|
||||
<% } %>
|
||||
<% } %>
|
||||
|
||||
<% if (pagination.current < pagination.total) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link"
|
||||
href="?page=<%= pagination.current + 1 %><%= query.search ? '&search=' + query.search : '' %><%= query.status ? '&status=' + query.status : '' %><%= query.category ? '&category=' + query.category : '' %>">Next</a>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</nav>
|
||||
<% } %>
|
||||
<% } else { %>
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-blog text-muted mb-3" style="font-size: 3rem;"></i>
|
||||
<h5 class="text-muted mb-3">No Blog Posts Found</h5>
|
||||
<a href="/admin/blog/create" class="btn btn-primary">
|
||||
<i class="fas fa-plus-circle me-1"></i>Create First Blog Post
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Blog Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteBlogModal" tabindex="-1" aria-labelledby="deleteBlogModalLabel" aria-hidden="true"
|
||||
data-bs-backdrop="true" data-bs-keyboard="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h5 class="modal-title" id="deleteBlogModalLabel">
|
||||
<i class="fas fa-trash me-2"></i>Confirm Delete
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete the blog post "<span id="deleteBlogTitle" class="fw-bold"></span>"?
|
||||
</p>
|
||||
<p class="text-danger mb-0">
|
||||
<small>
|
||||
<i class="fas fa-exclamation-triangle me-1"></i>This action cannot be
|
||||
undone.</small>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<form id="deleteBlogForm" method="POST" style="display: inline;">
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="fas fa-trash me-1"></i>Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Fix modal z-index - must be higher than everything else */
|
||||
#deleteBlogModal {
|
||||
z-index: 2050 !important;
|
||||
}
|
||||
|
||||
#deleteBlogModal .modal-dialog {
|
||||
z-index: 2060 !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#deleteBlogModal .modal-content {
|
||||
z-index: 2070 !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Ensure modal is clickable */
|
||||
#deleteBlogModal.show {
|
||||
display: block !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () { // Initialize modal instance once
|
||||
const deleteModalElement = document.getElementById('deleteBlogModal');
|
||||
const deleteModal = new bootstrap.Modal(deleteModalElement, {
|
||||
backdrop: false,
|
||||
keyboard: true,
|
||||
focus: true
|
||||
});
|
||||
|
||||
// Handle delete buttons
|
||||
document.querySelectorAll('[data-custom-modal="open"]').forEach(button => {
|
||||
button.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const blogId = this.getAttribute('data-id');
|
||||
const blogTitle = this.getAttribute('data-title');
|
||||
|
||||
// Set blog title in modal
|
||||
document.getElementById('deleteBlogTitle').textContent = blogTitle;
|
||||
|
||||
// Set form action
|
||||
document.getElementById('deleteBlogForm').action = `/admin/blog/${blogId}/delete`;
|
||||
|
||||
// Show Bootstrap modal
|
||||
deleteModal.show();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
107
views/admin/certificate/create.ejs
Normal file
107
views/admin/certificate/create.ejs
Normal file
@@ -0,0 +1,107 @@
|
||||
<div class="page-title-area">
|
||||
<div>
|
||||
<h1>Create Certificate</h1>
|
||||
<p class="subtitle">Register a new certificate record</p>
|
||||
</div>
|
||||
<a href="/admin/certificate" class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> Back</a>
|
||||
</div>
|
||||
|
||||
<% if (typeof error !== 'undefined' && error) { %>
|
||||
<div class="alert d-flex align-items-center gap-2 mb-3" style="background:var(--danger-soft);color:var(--danger-color);border:none;border-radius:var(--border-radius-sm);">
|
||||
<i class="fas fa-exclamation-circle"></i> <%= error %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/admin/certificate/create" enctype="multipart/form-data">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-8">
|
||||
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-id-card"></i> Basic Information</h5></div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Certificate No. <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="certification_number" required
|
||||
value="<%= (typeof formData !== 'undefined' && formData) ? formData.certification_number || '' : '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Student Full Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="student_name" required
|
||||
value="<%= (typeof formData !== 'undefined' && formData) ? formData.student_name || '' : '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Program / Title <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="program_name" required
|
||||
value="<%= (typeof formData !== 'undefined' && formData) ? formData.program_name || '' : '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Passport No.</label>
|
||||
<input type="text" class="form-control" name="passport_number"
|
||||
value="<%= (typeof formData !== 'undefined' && formData) ? formData.passport_number || '' : '' %>">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Address</label>
|
||||
<input type="text" class="form-control" name="address"
|
||||
value="<%= (typeof formData !== 'undefined' && formData) ? formData.address || '' : '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-tags"></i> Classification</h5></div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Department <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="department" required>
|
||||
<option value="">-- Select --</option>
|
||||
<% departments.forEach(d => { %>
|
||||
<option value="<%= d._id %>" <%= (typeof formData !== 'undefined' && formData && formData.department === d._id.toString()) ? 'selected' : '' %>><%= d.name %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Level <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="level" required>
|
||||
<option value="">-- Select --</option>
|
||||
<% levels.forEach(l => { %>
|
||||
<option value="<%= l._id %>" <%= (typeof formData !== 'undefined' && formData && formData.level === l._id.toString()) ? 'selected' : '' %>><%= l.type %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Issue Date <span class="text-danger">*</span></label>
|
||||
<input type="date" class="form-control" name="issued_date" required
|
||||
value="<%= (typeof formData !== 'undefined' && formData && formData.issued_date) ? formData.issued_date.toString().substring(0,10) : '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="active">Active</option>
|
||||
<option value="revoked" <%= (typeof formData !== 'undefined' && formData && formData.status === 'revoked') ? 'selected' : '' %>>Revoked</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-image"></i> Certificate Image</h5></div>
|
||||
<div class="card-body">
|
||||
<input type="file" class="form-control" name="certificate_image" accept="image/*">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card border-0">
|
||||
<div class="card-body d-flex flex-column gap-2">
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="fas fa-save"></i> Create Certificate</button>
|
||||
<a href="/admin/certificate" class="btn btn-outline-secondary w-100">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
107
views/admin/certificate/edit.ejs
Normal file
107
views/admin/certificate/edit.ejs
Normal file
@@ -0,0 +1,107 @@
|
||||
<div class="page-title-area">
|
||||
<div>
|
||||
<h1>Edit Certificate</h1>
|
||||
<p class="subtitle"><code style="font-size:0.8rem;color:var(--info-color)"><%= cert.certification_number %></code></p>
|
||||
</div>
|
||||
<a href="/admin/certificate" class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> Back</a>
|
||||
</div>
|
||||
|
||||
<% if (typeof error !== 'undefined' && error) { %>
|
||||
<div class="alert d-flex align-items-center gap-2 mb-3" style="background:var(--danger-soft);color:var(--danger-color);border:none;border-radius:var(--border-radius-sm);">
|
||||
<i class="fas fa-exclamation-circle"></i> <%= error %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/admin/certificate/<%= cert._id %>/edit" enctype="multipart/form-data">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-8">
|
||||
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-id-card"></i> Basic Information</h5></div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Certificate No. <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="certification_number" required value="<%= cert.certification_number %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Student Full Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="student_name" required value="<%= cert.student_name %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Program / Title <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="program_name" required value="<%= cert.program_name %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Passport No.</label>
|
||||
<input type="text" class="form-control" name="passport_number" value="<%= cert.passport_number || '' %>">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Address</label>
|
||||
<input type="text" class="form-control" name="address" value="<%= cert.address || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-tags"></i> Classification</h5></div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Department <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="department" required>
|
||||
<option value="">-- Select --</option>
|
||||
<% departments.forEach(d => { %>
|
||||
<option value="<%= d._id %>" <%= (cert.department && cert.department._id && cert.department._id.toString() === d._id.toString()) ? 'selected' : '' %>><%= d.name %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Level <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="level" required>
|
||||
<option value="">-- Select --</option>
|
||||
<% levels.forEach(l => { %>
|
||||
<option value="<%= l._id %>" <%= (cert.level && cert.level._id && cert.level._id.toString() === l._id.toString()) ? 'selected' : '' %>><%= l.type %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Issue Date <span class="text-danger">*</span></label>
|
||||
<input type="date" class="form-control" name="issued_date" required value="<%= cert.issued_date ? new Date(cert.issued_date).toISOString().substring(0,10) : '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="active" <%= cert.status === 'active' ? 'selected' : '' %>>Active</option>
|
||||
<option value="revoked" <%= cert.status === 'revoked' ? 'selected' : '' %>>Revoked</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-image"></i> Certificate Image</h5></div>
|
||||
<div class="card-body">
|
||||
<% if (cert.certificate_image) { %>
|
||||
<div class="mb-2">
|
||||
<img src="/admin/files/<%= cert.certificate_image %>" alt="Certificate image" class="img-thumbnail" style="max-height:110px;border-radius:var(--border-radius-sm);">
|
||||
</div>
|
||||
<% } %>
|
||||
<input type="file" class="form-control" name="certificate_image" accept="image/*">
|
||||
<div class="form-text">Leave empty to keep current image.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card border-0">
|
||||
<div class="card-body d-flex flex-column gap-2">
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="fas fa-save"></i> Save Changes</button>
|
||||
<a href="/admin/certificate" class="btn btn-outline-secondary w-100">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
102
views/admin/certificate/index.ejs
Normal file
102
views/admin/certificate/index.ejs
Normal file
@@ -0,0 +1,102 @@
|
||||
<div class="page-title-area">
|
||||
<div>
|
||||
<h1>Certificates</h1>
|
||||
<p class="subtitle">Certificate records</p>
|
||||
</div>
|
||||
<a href="/admin/certificate/create" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> New Certificate
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Filter -->
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-body" style="padding:1rem 1.25rem;">
|
||||
<form method="GET" action="/admin/certificate">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<div class="position-relative search-bar">
|
||||
<i class="fas fa-search search-icon"></i>
|
||||
<input type="text" class="form-control" name="search" placeholder="Search name, number..." value="<%= query.search || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" name="status">
|
||||
<option value="">All Status</option>
|
||||
<option value="active" <%= query.status === 'active' ? 'selected' : '' %>>Active</option>
|
||||
<option value="revoked" <%= query.status === 'revoked' ? 'selected' : '' %>>Revoked</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1 d-flex gap-1">
|
||||
<button type="submit" class="btn btn-primary flex-fill"><i class="fas fa-search"></i></button>
|
||||
<% if (query.search || query.status) { %>
|
||||
<a href="/admin/certificate" class="btn btn-outline-secondary"><i class="fas fa-times"></i></a>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card border-0">
|
||||
<div class="card-body p-0">
|
||||
<% if (certificates && certificates.length > 0) { %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px">#</th>
|
||||
<th>Certificate No.</th>
|
||||
<th>Full Name</th>
|
||||
<th>Program</th>
|
||||
<th>Department</th>
|
||||
<th>Level</th>
|
||||
<th>Issue Date</th>
|
||||
<th>Status</th>
|
||||
<th style="width:90px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% certificates.forEach((c, i) => { %>
|
||||
<tr>
|
||||
<td style="color:var(--text-muted)"><%= i + 1 %></td>
|
||||
<td><code style="font-size:0.8rem;color:var(--info-color)"><%= c.certification_number %></code></td>
|
||||
<td style="font-weight:500"><%= c.student_name %></td>
|
||||
<td style="font-size:0.8125rem;color:var(--text-muted)"><%= c.program_name %></td>
|
||||
<td style="font-size:0.8125rem"><%= c.department ? c.department.name : '—' %></td>
|
||||
<td style="font-size:0.8125rem"><%= c.level ? c.level.type : '—' %></td>
|
||||
<td style="font-size:0.8125rem;color:var(--text-muted)"><%= c.issued_date ? new Date(c.issued_date).toLocaleDateString('en-GB') : '—' %></td>
|
||||
<td>
|
||||
<% if (c.status === 'active') { %>
|
||||
<span class="badge bg-soft-success">Active</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-soft-danger">Revoked</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-actions">
|
||||
<a href="/admin/certificate/<%= c._id %>/edit" class="btn btn-sm btn-outline-primary btn-icon" title="Edit">
|
||||
<i class="fas fa-pen"></i>
|
||||
</a>
|
||||
<form method="POST" action="/admin/certificate/<%= c._id %>/delete" style="display:inline;" onsubmit="return confirm('Delete this certificate?')">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger btn-icon" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon"><i class="fas fa-certificate"></i></div>
|
||||
<h5>No certificates found</h5>
|
||||
<p>Create the first certificate record.</p>
|
||||
<a href="/admin/certificate/create" class="btn btn-primary"><i class="fas fa-plus"></i> Create</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,456 +1,155 @@
|
||||
<div class="container mt-4 mb-4">
|
||||
<h1 class="page-title mb-4">Dashboard</h1>
|
||||
<!-- Page Header -->
|
||||
<div class="page-title-area mb-4">
|
||||
<div>
|
||||
<h1 style="font-size:1.4rem;font-weight:700;color:var(--primary-color);margin:0;">Dashboard</h1>
|
||||
<p class="text-muted mb-0" style="font-size:0.8125rem;">ULDP Management System overview</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="/admin/qualification/create" class="btn btn-primary">
|
||||
<i class="fas fa-graduation-cap"></i> New Qualification
|
||||
</a>
|
||||
<a href="/admin/certificate/create" class="btn btn-outline-primary">
|
||||
<i class="fas fa-certificate"></i> New Certificate
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0" style="color: var(--primary-color)">Quick Management</h5>
|
||||
<!-- Stats Grid -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-md-4 col-xl-2">
|
||||
<div class="stat-card primary">
|
||||
<div class="stat-card-value"><%= total %></div>
|
||||
<div class="stat-card-label">Total Records</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="row g-0">
|
||||
<!-- Home -->
|
||||
<div class="col-md-4 border-end">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-home fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Home</h5>
|
||||
<p class="text-muted mb-0 small">Manage homepage</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/home" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-4 col-xl-2">
|
||||
<div class="stat-card accent">
|
||||
<div class="stat-card-value"><%= qualificationCount %></div>
|
||||
<div class="stat-card-label">Qualifications</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-4 col-xl-2">
|
||||
<div class="stat-card info">
|
||||
<div class="stat-card-value"><%= certificationCount %></div>
|
||||
<div class="stat-card-label">Certificates</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-4 col-xl-2">
|
||||
<div class="stat-card success">
|
||||
<div class="stat-card-value"><%= activeCount %></div>
|
||||
<div class="stat-card-label">Active</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-4 col-xl-2">
|
||||
<div class="stat-card danger">
|
||||
<div class="stat-card-value"><%= revokedCount %></div>
|
||||
<div class="stat-card-label">Revoked</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-4 col-xl-2">
|
||||
<div class="stat-card warning">
|
||||
<div class="stat-card-value"><%= recentCount %></div>
|
||||
<div class="stat-card-label">New (30d)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header & Menu -->
|
||||
<div class="col-md-4 border-end">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-bars fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Header & Menu</h5>
|
||||
<p class="text-muted mb-0 small">Manage header & menu</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/header" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
<div class="row g-3">
|
||||
<!-- Recent Qualifications -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-0">
|
||||
<div class="card-header">
|
||||
<h5 class="card-header-title"><i class="fas fa-graduation-cap"></i> Latest Qualifications</h5>
|
||||
<a href="/admin/qualification" class="btn btn-sm btn-outline-primary">View All</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<% if (recentQualifications && recentQualifications.length > 0) { %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Qual. No.</th>
|
||||
<th>Full Name</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% recentQualifications.forEach(q => { %>
|
||||
<tr>
|
||||
<td><code style="font-size:0.8rem;color:var(--primary-color)"><%= q.qualification_number %></code></td>
|
||||
<td style="font-weight:500;font-size:0.8125rem"><%= q.student_name %></td>
|
||||
<td>
|
||||
<% if (q.topic_name) { %>
|
||||
<span class="badge badge-soft-primary">PhD</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-soft-info">MBA/Master</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge <%= q.status === 'active' ? 'bg-soft-success' : 'bg-soft-danger' %>">
|
||||
<%= q.status %>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="col-md-4">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-window-minimize fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Footer</h5>
|
||||
<p class="text-muted mb-0 small">Manage footer</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/footer" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
<% } else { %>
|
||||
<div class="empty-state" style="padding:2rem 1rem;">
|
||||
<div class="empty-state-icon"><i class="fas fa-graduation-cap"></i></div>
|
||||
<h5>No qualifications yet</h5>
|
||||
<a href="/admin/qualification/create" class="btn btn-primary btn-sm"><i class="fas fa-plus"></i> Create</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- About Us -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-users fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">About Us</h5>
|
||||
<p class="text-muted mb-0 small">Manage about us</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/about-us" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-envelope fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Contact</h5>
|
||||
<p class="text-muted mb-0 small">Manage contact</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/contact" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Appointment -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-calendar-check fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Appointment</h5>
|
||||
<p class="text-muted mb-0 small">Manage appointment page</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/appointment" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pricing -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-tags fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Pricing</h5>
|
||||
<p class="text-muted mb-0 small">Manage pricing page</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/pricing" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Services -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-concierge-bell fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Services</h5>
|
||||
<p class="text-muted mb-0 small">Manage services</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/service" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blog -->
|
||||
<div class="col-md-4 border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-blog fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Blog</h5>
|
||||
<p class="text-muted mb-0 small">Manage blog posts</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/blog" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Visa -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-passport fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Visa</h5>
|
||||
<p class="text-muted mb-0 small">Manage visa countries</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/visa" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Links Section -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">API Endpoints</h5>
|
||||
<span class="badge bg-primary">6 APIs</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>API Name</th>
|
||||
<th>Endpoint</th>
|
||||
<th>Method</th>
|
||||
<th>Description</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
|
||||
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-bars" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>Menu Header API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/header</code></td>
|
||||
<td>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>API to get menu header data</td>
|
||||
<td>
|
||||
<a href="/api/header" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
|
||||
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-home" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>Home API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/home</code></td>
|
||||
<td>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>API to get homepage data</td>
|
||||
<td>
|
||||
<a href="/api/home" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
|
||||
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-info-circle" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>About API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/about</code></td>
|
||||
<td>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>API to get about page data</td>
|
||||
<td>
|
||||
<a href="/api/about" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
|
||||
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-users" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>About Us API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/about-us</code></td>
|
||||
<td>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>API to get about us data</td>
|
||||
<td>
|
||||
<a href="/api/about-us" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
|
||||
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-envelope" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>Contact API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/contact</code></td>
|
||||
<td>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>API to get contact data</td>
|
||||
<td>
|
||||
<a href="/api/contact" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
|
||||
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-blog" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>Blog API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/blog</code></td>
|
||||
<td>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>API to get blog posts</td>
|
||||
<td>
|
||||
<a href="/api/blog" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Recent Certificates -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-0">
|
||||
<div class="card-header">
|
||||
<h5 class="card-header-title"><i class="fas fa-certificate"></i> Latest Certificates</h5>
|
||||
<a href="/admin/certificate" class="btn btn-sm btn-outline-primary">View All</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Info -->
|
||||
<div class="card">
|
||||
<div class="card-header" style="
|
||||
background: #0a2347;
|
||||
color: white;
|
||||
">
|
||||
<h5 class="mb-0">System Information</h5>
|
||||
</div>
|
||||
<div class="card-body" style="background-color: #f8faf8">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 40px; height: 40px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-info-circle" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<% if (recentCertificates && recentCertificates.length > 0) { %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cert. No.</th>
|
||||
<th>Full Name</th>
|
||||
<th>Program</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% recentCertificates.forEach(c => { %>
|
||||
<tr>
|
||||
<td><code style="font-size:0.8rem;color:var(--info-color)"><%= c.certification_number %></code></td>
|
||||
<td style="font-weight:500;font-size:0.8125rem"><%= c.student_name %></td>
|
||||
<td style="font-size:0.8125rem;color:var(--text-muted)"><%= c.program_name %></td>
|
||||
<td>
|
||||
<span class="badge <%= c.status === 'active' ? 'bg-soft-success' : 'bg-soft-danger' %>">
|
||||
<%= c.status %>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small">Version</div>
|
||||
<div class="fw-bold" style="color: var(--primary-color)">
|
||||
CMS.HAILearning v1.0.0
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="empty-state" style="padding:2rem 1rem;">
|
||||
<div class="empty-state-icon"><i class="fas fa-certificate"></i></div>
|
||||
<h5>No certificates yet</h5>
|
||||
<a href="/admin/certificate/create" class="btn btn-primary btn-sm"><i class="fas fa-plus"></i> Create</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 40px; height: 40px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-user" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small">Logged in as</div>
|
||||
<div class="fw-bold" style="color: var(--primary-color);">
|
||||
<%= user.username %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert mt-3 mb-0"
|
||||
style="background-color: rgba(184, 183, 106, 0.05); border-left: 4px solid var(--primary-color);" role="alert">
|
||||
<div class="d-flex">
|
||||
<div class="me-3">
|
||||
<i class="fas fa-lightbulb fa-lg" style="color: var(--primary-color)"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-1" style="color: var(--primary-color)">Quick Tip</h6>
|
||||
<p class="mb-0 text-muted">
|
||||
Click the Edit button to make changes to your data.
|
||||
</p>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page-title {
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-dark);
|
||||
border-color: var(--primary-dark);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-outline-primary:hover {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-header h5 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background-color: var(--primary-color) !important;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
166
views/admin/degree/create.ejs
Normal file
166
views/admin/degree/create.ejs
Normal file
@@ -0,0 +1,166 @@
|
||||
<!-- Page Header -->
|
||||
<div class="page-title-area">
|
||||
<div>
|
||||
<h1>Create New Degree</h1>
|
||||
<p class="subtitle">Fill in the details below to register a new degree</p>
|
||||
</div>
|
||||
<a href="/admin/degree" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<% if (typeof error !== 'undefined' && error) { %>
|
||||
<div class="alert alert-danger d-flex align-items-center gap-2 mb-3" style="border-radius:var(--border-radius-sm);border:none;background:var(--danger-soft);color:var(--danger-color);">
|
||||
<i class="fas fa-exclamation-circle"></i> <%= error %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/admin/degree/create" enctype="multipart/form-data">
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- Left column -->
|
||||
<div class="col-lg-8">
|
||||
|
||||
<!-- Basic Info -->
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="card-header-title"><i class="fas fa-id-card"></i> Basic Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Qualification No. <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="qualification_number" required
|
||||
value="<%= (typeof formData !== 'undefined' && formData) ? formData.qualification_number || '' : '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Certification No.</label>
|
||||
<input type="text" class="form-control" name="certification_number"
|
||||
value="<%= (typeof formData !== 'undefined' && formData) ? formData.certification_number || '' : '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Student Full Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="student_name" required
|
||||
value="<%= (typeof formData !== 'undefined' && formData) ? formData.student_name || '' : '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Program Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="program_name" required
|
||||
value="<%= (typeof formData !== 'undefined' && formData) ? formData.program_name || '' : '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Passport No.</label>
|
||||
<input type="text" class="form-control" name="passport_number"
|
||||
value="<%= (typeof formData !== 'undefined' && formData) ? formData.passport_number || '' : '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Address</label>
|
||||
<input type="text" class="form-control" name="address"
|
||||
value="<%= (typeof formData !== 'undefined' && formData) ? formData.address || '' : '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Classification -->
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="card-header-title"><i class="fas fa-tags"></i> Classification</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Type <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="type" required>
|
||||
<option value="">-- Select --</option>
|
||||
<option value="qualification" <%= (typeof formData !== 'undefined' && formData && formData.type === 'qualification') ? 'selected' : '' %>>Qualification</option>
|
||||
<option value="certification" <%= (typeof formData !== 'undefined' && formData && formData.type === 'certification') ? 'selected' : '' %>>Certification</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Department <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="department" required>
|
||||
<option value="">-- Select --</option>
|
||||
<% departments.forEach(dept => { %>
|
||||
<option value="<%= dept._id %>" <%= (typeof formData !== 'undefined' && formData && formData.department === dept._id.toString()) ? 'selected' : '' %>><%= dept.name %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Level <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="level" required>
|
||||
<option value="">-- Select --</option>
|
||||
<% levels.forEach(lvl => { %>
|
||||
<option value="<%= lvl._id %>" <%= (typeof formData !== 'undefined' && formData && formData.level === lvl._id.toString()) ? 'selected' : '' %>><%= lvl.type %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Issue Date <span class="text-danger">*</span></label>
|
||||
<input type="date" class="form-control" name="issued_date" required
|
||||
value="<%= (typeof formData !== 'undefined' && formData && formData.issued_date) ? formData.issued_date.toString().substring(0,10) : '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="active" <%= (typeof formData === 'undefined' || !formData || formData.status !== 'revoked') ? 'selected' : '' %>>Active</option>
|
||||
<option value="revoked" <%= (typeof formData !== 'undefined' && formData && formData.status === 'revoked') ? 'selected' : '' %>>Revoked</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Thesis -->
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="card-header-title"><i class="fas fa-book-open"></i> Thesis / Topic</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Topic Title</label>
|
||||
<input type="text" class="form-control" name="topic_name"
|
||||
value="<%= (typeof formData !== 'undefined' && formData) ? formData.topic_name || '' : '' %>">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Short Description</label>
|
||||
<textarea class="form-control" name="topic_short_desc" rows="3"><%= (typeof formData !== 'undefined' && formData) ? formData.topic_short_desc || '' : '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Right column: Images -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="card-header-title"><i class="fas fa-images"></i> Images</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Degree Image</label>
|
||||
<input type="file" class="form-control" name="degree_image" accept="image/*">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Certificate Image</label>
|
||||
<input type="file" class="form-control" name="certificate_image" accept="image/*">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="card border-0">
|
||||
<div class="card-body d-flex flex-column gap-2">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="fas fa-save"></i> Create Degree
|
||||
</button>
|
||||
<a href="/admin/degree" class="btn btn-outline-secondary w-100">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
170
views/admin/degree/edit.ejs
Normal file
170
views/admin/degree/edit.ejs
Normal file
@@ -0,0 +1,170 @@
|
||||
<!-- Page Header -->
|
||||
<div class="page-title-area">
|
||||
<div>
|
||||
<h1>Edit Degree</h1>
|
||||
<p class="subtitle"><code style="font-size:0.8rem;color:var(--accent-color)"><%= degree.qualification_number %></code></p>
|
||||
</div>
|
||||
<a href="/admin/degree" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<% if (typeof error !== 'undefined' && error) { %>
|
||||
<div class="alert d-flex align-items-center gap-2 mb-3" style="border-radius:var(--border-radius-sm);border:none;background:var(--danger-soft);color:var(--danger-color);">
|
||||
<i class="fas fa-exclamation-circle"></i> <%= error %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/admin/degree/<%= degree._id %>/edit" enctype="multipart/form-data">
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- Left column -->
|
||||
<div class="col-lg-8">
|
||||
|
||||
<!-- Basic Info -->
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="card-header-title"><i class="fas fa-id-card"></i> Basic Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Qualification No. <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="qualification_number" required value="<%= degree.qualification_number %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Certification No.</label>
|
||||
<input type="text" class="form-control" name="certification_number" value="<%= degree.certification_number || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Student Full Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="student_name" required value="<%= degree.student_name %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Program Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="program_name" required value="<%= degree.program_name %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Passport No.</label>
|
||||
<input type="text" class="form-control" name="passport_number" value="<%= degree.passport_number || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Address</label>
|
||||
<input type="text" class="form-control" name="address" value="<%= degree.address || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Classification -->
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="card-header-title"><i class="fas fa-tags"></i> Classification</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Type <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="type" required>
|
||||
<option value="qualification" <%= degree.type === 'qualification' ? 'selected' : '' %>>Qualification</option>
|
||||
<option value="certification" <%= degree.type === 'certification' ? 'selected' : '' %>>Certification</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Department <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="department" required>
|
||||
<option value="">-- Select --</option>
|
||||
<% departments.forEach(dept => { %>
|
||||
<option value="<%= dept._id %>" <%= (degree.department && degree.department._id && degree.department._id.toString() === dept._id.toString()) ? 'selected' : '' %>><%= dept.name %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Level <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="level" required>
|
||||
<option value="">-- Select --</option>
|
||||
<% levels.forEach(lvl => { %>
|
||||
<option value="<%= lvl._id %>" <%= (degree.level && degree.level._id && degree.level._id.toString() === lvl._id.toString()) ? 'selected' : '' %>><%= lvl.type %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Issue Date <span class="text-danger">*</span></label>
|
||||
<input type="date" class="form-control" name="issued_date" required
|
||||
value="<%= degree.issued_date ? new Date(degree.issued_date).toISOString().substring(0,10) : '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="active" <%= degree.status === 'active' ? 'selected' : '' %>>Active</option>
|
||||
<option value="revoked" <%= degree.status === 'revoked' ? 'selected' : '' %>>Revoked</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Thesis -->
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="card-header-title"><i class="fas fa-book-open"></i> Thesis / Topic</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Topic Title</label>
|
||||
<input type="text" class="form-control" name="topic_name" value="<%= degree.topic_name || '' %>">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Short Description</label>
|
||||
<textarea class="form-control" name="topic_short_desc" rows="3"><%= degree.topic_short_desc || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Right column -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="card-header-title"><i class="fas fa-images"></i> Images</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Degree Image</label>
|
||||
<% if (degree.degree_image) { %>
|
||||
<div class="mb-2">
|
||||
<img src="/admin/files/<%= degree.degree_image %>" alt="Degree image" class="img-thumbnail" style="max-height:110px;border-radius:var(--border-radius-sm);">
|
||||
</div>
|
||||
<% } %>
|
||||
<input type="file" class="form-control" name="degree_image" accept="image/*">
|
||||
<div class="form-text">Leave empty to keep current image.</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Certificate Image</label>
|
||||
<% if (degree.certificate_image) { %>
|
||||
<div class="mb-2">
|
||||
<img src="/admin/files/<%= degree.certificate_image %>" alt="Certificate image" class="img-thumbnail" style="max-height:110px;border-radius:var(--border-radius-sm);">
|
||||
</div>
|
||||
<% } %>
|
||||
<input type="file" class="form-control" name="certificate_image" accept="image/*">
|
||||
<div class="form-text">Leave empty to keep current image.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="card border-0">
|
||||
<div class="card-body d-flex flex-column gap-2">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="fas fa-save"></i> Save Changes
|
||||
</button>
|
||||
<a href="/admin/degree" class="btn btn-outline-secondary w-100">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
142
views/admin/degree/index.ejs
Normal file
142
views/admin/degree/index.ejs
Normal file
@@ -0,0 +1,142 @@
|
||||
<!-- Page Header -->
|
||||
<div class="page-title-area">
|
||||
<div>
|
||||
<h1>Degree Management</h1>
|
||||
<p class="subtitle">All degrees registered in the system</p>
|
||||
</div>
|
||||
<a href="/admin/degree/create" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> New Degree
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Search & Filter -->
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-body" style="padding:1rem 1.25rem;">
|
||||
<form method="GET" action="/admin/degree">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<div class="position-relative search-bar">
|
||||
<i class="fas fa-search search-icon"></i>
|
||||
<input type="text" class="form-control" name="search" placeholder="Search name, number..." value="<%= query.search || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" name="type">
|
||||
<option value="">All Types</option>
|
||||
<option value="qualification" <%= query.type === 'qualification' ? 'selected' : '' %>>Qualification</option>
|
||||
<option value="certification" <%= query.type === 'certification' ? 'selected' : '' %>>Certification</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" name="department">
|
||||
<option value="">All Departments</option>
|
||||
<% departments.forEach(dept => { %>
|
||||
<option value="<%= dept._id %>" <%= query.department === dept._id.toString() ? 'selected' : '' %>><%= dept.name %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" name="level">
|
||||
<option value="">All Levels</option>
|
||||
<% levels.forEach(lvl => { %>
|
||||
<option value="<%= lvl._id %>" <%= query.level === lvl._id.toString() ? 'selected' : '' %>><%= lvl.type %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" name="status">
|
||||
<option value="">All Status</option>
|
||||
<option value="active" <%= query.status === 'active' ? 'selected' : '' %>>Active</option>
|
||||
<option value="revoked" <%= query.status === 'revoked' ? 'selected' : '' %>>Revoked</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1 d-flex gap-1">
|
||||
<button type="submit" class="btn btn-primary flex-fill">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
<% if (query.search || query.type || query.department || query.level || query.status) { %>
|
||||
<a href="/admin/degree" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times"></i>
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card border-0">
|
||||
<div class="card-body p-0">
|
||||
<% if (degrees && degrees.length > 0) { %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px">#</th>
|
||||
<th>Qual. No.</th>
|
||||
<th>Cert. No.</th>
|
||||
<th>Full Name</th>
|
||||
<th>Program</th>
|
||||
<th>Type</th>
|
||||
<th>Department</th>
|
||||
<th>Level</th>
|
||||
<th>Issue Date</th>
|
||||
<th>Status</th>
|
||||
<th style="width:90px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% degrees.forEach((degree, index) => { %>
|
||||
<tr>
|
||||
<td style="color:var(--text-muted)"><%= index + 1 %></td>
|
||||
<td><code style="font-size:0.8rem;color:var(--primary-color)"><%= degree.qualification_number %></code></td>
|
||||
<td><code style="font-size:0.8rem;color:var(--text-muted)"><%= degree.certification_number || '—' %></code></td>
|
||||
<td style="font-weight:500"><%= degree.student_name %></td>
|
||||
<td style="color:var(--text-muted);font-size:0.8125rem"><%= degree.program_name %></td>
|
||||
<td>
|
||||
<% if (degree.type === 'qualification') { %>
|
||||
<span class="badge badge-soft-primary">Qualification</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-soft-info">Certification</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td style="font-size:0.8125rem"><%= degree.department ? degree.department.name : '—' %></td>
|
||||
<td style="font-size:0.8125rem"><%= degree.level ? degree.level.type : '—' %></td>
|
||||
<td style="color:var(--text-muted);font-size:0.8125rem"><%= degree.issued_date ? new Date(degree.issued_date).toLocaleDateString('en-GB') : '—' %></td>
|
||||
<td>
|
||||
<% if (degree.status === 'active') { %>
|
||||
<span class="badge bg-soft-success">Active</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-soft-danger">Revoked</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-actions">
|
||||
<a href="/admin/degree/<%= degree._id %>/edit" class="btn btn-sm btn-outline-primary btn-icon" title="Edit">
|
||||
<i class="fas fa-pen"></i>
|
||||
</a>
|
||||
<form method="POST" action="/admin/degree/<%= degree._id %>/delete" style="display:inline;" onsubmit="return confirm('Delete this degree?')">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger btn-icon" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon"><i class="fas fa-graduation-cap"></i></div>
|
||||
<h5>No degrees found</h5>
|
||||
<p>Try adjusting your filters or create a new degree.</p>
|
||||
<a href="/admin/degree/create" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Create First Degree
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,126 +1,112 @@
|
||||
<!-- Header section -->
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">Department Management</h1>
|
||||
<p class="text-muted mb-0">Manage all departments in the system</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/department/create" class="btn btn-primary">
|
||||
<i class="fas fa-plus-circle me-1"></i>Create New Department
|
||||
</a>
|
||||
<!-- Page Header -->
|
||||
<div class="page-title-area">
|
||||
<div>
|
||||
<h1>Department Management</h1>
|
||||
<p class="subtitle">All departments registered in the system</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Add Form -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0">
|
||||
<div class="card-header">
|
||||
<h5 class="card-header-title"><i class="fas fa-plus-circle"></i> Add Department</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/admin/department/create">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Department Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="name" placeholder="Enter department name..." required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="fas fa-plus"></i> Add Department
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<% if (departments && departments.length > 0) { %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead>
|
||||
<!-- Table -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card border-0">
|
||||
<div class="card-header">
|
||||
<h5 class="card-header-title"><i class="fas fa-building"></i> Departments</h5>
|
||||
<span class="badge badge-soft-primary"><%= departments ? departments.length : 0 %> total</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<% if (departments && departments.length > 0) { %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:50px">#</th>
|
||||
<th>Department Name</th>
|
||||
<th>Slug</th>
|
||||
<th style="width:130px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% departments.forEach((department, index) => { %>
|
||||
<tr>
|
||||
<th scope="col" style="width: 50px">#</th>
|
||||
<th scope="col">Department Name</th>
|
||||
<th scope="col">Slug</th>
|
||||
<th scope="col" style="width: 200px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% departments.forEach((department, index) => { %>
|
||||
<tr>
|
||||
<td><%= index + 1 %></td>
|
||||
<td>
|
||||
<span class="fw-medium"><%= department.name %></span>
|
||||
</td>
|
||||
<td>
|
||||
<code class="text-muted"><%= department.slug %></code>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="/admin/department/edit/<%= department._id %>" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-edit me-1"></i>Edit
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||
data-custom-modal="open"
|
||||
data-id="<%= department._id %>"
|
||||
data-name="<%= department.name %>">
|
||||
<i class="fas fa-trash-alt me-1"></i>Delete
|
||||
<td style="color:var(--text-muted)"><%= index + 1 %></td>
|
||||
<td>
|
||||
<span class="dept-name-display-<%= department._id %>" style="font-weight:500"><%= department.name %></span>
|
||||
<form method="POST" action="/admin/department/<%= department._id %>/edit"
|
||||
class="dept-edit-form-<%= department._id %> d-none d-flex gap-2 align-items-center">
|
||||
<input type="text" class="form-control form-control-sm" name="name" value="<%= department.name %>" required>
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary btn-icon" title="Save">
|
||||
<i class="fas fa-check"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary btn-icon" title="Cancel"
|
||||
onclick="cancelEdit('<%= department._id %>')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td><code style="font-size:0.8rem;color:var(--text-muted)"><%= department.slug %></code></td>
|
||||
<td>
|
||||
<div class="table-actions">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary btn-icon" title="Edit"
|
||||
onclick="startEdit('<%= department._id %>')">
|
||||
<i class="fas fa-pen"></i>
|
||||
</button>
|
||||
<form method="POST" action="/admin/department/<%= department._id %>/delete" style="display:inline;"
|
||||
onsubmit="return confirm('Delete department \"<%= department.name %>\"?')">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger btn-icon" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-folder-open text-muted mb-3" style="font-size: 3rem;"></i>
|
||||
<h5 class="text-muted mb-3">No Departments Found</h5>
|
||||
<a href="/admin/department/create" class="btn btn-primary">
|
||||
<i class="fas fa-plus-circle me-1"></i>Create First Department
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon"><i class="fas fa-building"></i></div>
|
||||
<h5>No departments yet</h5>
|
||||
<p>Use the form on the left to add the first department.</p>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Modal -->
|
||||
<div id="customModal" class="custom-modal">
|
||||
<div class="custom-modal-content">
|
||||
<div class="custom-modal-header">
|
||||
<h5 class="custom-modal-title">Delete Confirmation</h5>
|
||||
<button type="button" class="custom-modal-close">×</button>
|
||||
</div>
|
||||
<div class="custom-modal-body">
|
||||
<p id="modalMessage">Are you sure you want to delete this department?</p>
|
||||
</div>
|
||||
<div class="custom-modal-footer">
|
||||
<button type="button" class="btn btn-secondary custom-modal-cancel">Cancel</button>
|
||||
<button type="button" class="btn btn-danger custom-modal-ok">Delete Permanently</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import custom modal CSS -->
|
||||
<link rel="stylesheet" href="/css/custom-modal.css">
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Khởi tạo modal tùy chỉnh
|
||||
CustomModal.init('customModal', {
|
||||
closeOnOutsideClick: true,
|
||||
animationDuration: 300
|
||||
});
|
||||
|
||||
// Lắng nghe click vào nút xóa
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.getAttribute('data-custom-modal') === 'open' ||
|
||||
e.target.parentElement.getAttribute('data-custom-modal') === 'open') {
|
||||
|
||||
// Lấy button hoặc icon parent nếu click vào icon
|
||||
const button = e.target.getAttribute('data-custom-modal') === 'open' ?
|
||||
e.target : e.target.parentElement;
|
||||
|
||||
const id = button.getAttribute('data-id');
|
||||
const name = button.getAttribute('data-name');
|
||||
|
||||
// Sử dụng CustomModal.confirm thay vì xử lý trực tiếp
|
||||
CustomModal.confirm(
|
||||
`Are you sure you want to delete department "${name}"? This action cannot be undone.`,
|
||||
function() {
|
||||
// Hành động khi xác nhận
|
||||
window.location.href = `/admin/department/delete/${id}`;
|
||||
},
|
||||
null,
|
||||
'Delete Confirmation'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
function startEdit(id) {
|
||||
document.querySelector('.dept-name-display-' + id).classList.add('d-none');
|
||||
const form = document.querySelector('.dept-edit-form-' + id);
|
||||
form.classList.remove('d-none');
|
||||
form.classList.add('d-flex');
|
||||
}
|
||||
function cancelEdit(id) {
|
||||
document.querySelector('.dept-name-display-' + id).classList.remove('d-none');
|
||||
const form = document.querySelector('.dept-edit-form-' + id);
|
||||
form.classList.add('d-none');
|
||||
form.classList.remove('d-flex');
|
||||
}
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,661 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">Form Management</h1>
|
||||
<p class="text-muted mb-0">Edit form content for Academics page</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/academics" class="btn btn-outline-primary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Academics
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form id="formManagementForm" action="/admin/form/update" method="POST" class="content-with-fixed-buttons">
|
||||
<input type="hidden" name="admissionJson" id="admissionJson">
|
||||
<input type="hidden" name="applyJson" id="applyJson">
|
||||
<input type="hidden" name="applicationFormJson" id="applicationFormJson">
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#admission" role="tab">
|
||||
<i class="fas fa-door-open me-2"></i>Admission
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#apply" role="tab">
|
||||
<i class="fas fa-check-circle me-2"></i>Apply
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#application-form" role="tab">
|
||||
<i class="fas fa-file-alt me-2"></i>Application Form
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<!-- Admission Tab -->
|
||||
<div class="tab-pane fade show active" id="admission" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Background Image</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="admissionBgImage" name="admissionBgImage" value="<%= form.admission && form.admission.background_image ? form.admission.background_image : '' %>">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="admissionBgImage" data-image-type="academics">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="image-preview mt-2">
|
||||
<% if (form.admission && form.admission.background_image) { %>
|
||||
<img src="<%= form.admission.background_image %>" alt="Background preview" class="img-thumbnail" style="height:200px;width:100%;object-fit:cover;">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="admissionTitle" name="admissionTitle" value="<%= form.admission && form.admission.title ? form.admission.title : '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Year</label>
|
||||
<input type="text" class="form-control" id="admissionYear" name="admissionYear" value="<%= form.admission && form.admission.year ? form.admission.year : '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" id="admissionDescription" name="admissionDescription" rows="3"><%= form.admission && form.admission.description ? form.admission.description : '' %></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Admission Form Fields -->
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Form Fields</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="admissionFormFields">
|
||||
<% (form.admission && form.admission.form && form.admission.form.fields || []).forEach((field, index) => { %>
|
||||
<div class="mb-3 border p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Field Type</label>
|
||||
<input type="text" class="form-control" value="<%= field.type %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Placeholder</label>
|
||||
<input type="text" class="form-control" value="<%= field.placeholder %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Form Button Text</label>
|
||||
<input type="text" class="form-control" id="admissionFormButton" name="admissionFormButton" value="<%= form.admission && form.admission.form && form.admission.form.button ? form.admission.form.button.text : '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Form Button URL</label>
|
||||
<input type="text" class="form-control" id="admissionFormButtonUrl" name="admissionFormButtonUrl" value="<%= form.admission && form.admission.form && form.admission.form.button ? form.admission.form.button.url : '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Apply Tab -->
|
||||
<div class="tab-pane fade" id="apply" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="applyTitle" name="applyTitle" value="<%= form.apply && form.apply.title ? form.apply.title : '' %>">
|
||||
</div>
|
||||
|
||||
<!-- Apply Steps -->
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Application Steps</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="applySteps">
|
||||
<% (form.apply && form.apply.steps || []).forEach((step, index) => { %>
|
||||
<div class="mb-3 border p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" name="applyStepTitle_<%= index %>" value="<%= step.title %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" name="applyStepDescription_<%= index %>" rows="2"><%= step.description %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Application Form Tab -->
|
||||
<div class="tab-pane fade" id="application-form" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="applicationFormTitle" name="applicationFormTitle" value="<%= form.application_form && form.application_form.title ? form.application_form.title : '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Question</label>
|
||||
<input type="text" class="form-control" id="applicationFormQuestion" name="applicationFormQuestion" value="<%= form.application_form && form.application_form.question ? form.application_form.question : '' %>">
|
||||
</div>
|
||||
|
||||
<!-- Button Section -->
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Button</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Text</label>
|
||||
<input type="text" class="form-control" id="applicationFormButtonText" name="applicationFormButtonText" value="<%= form.application_form && form.application_form.button ? form.application_form.button.text : '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Icon</label>
|
||||
<input type="text" class="form-control" id="applicationFormButtonIcon" name="applicationFormButtonIcon" value="<%= form.application_form && form.application_form.button ? form.application_form.button.icon : '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">URL</label>
|
||||
<input type="text" class="form-control" id="applicationFormButtonUrl" name="applicationFormButtonUrl" value="<%= form.application_form && form.application_form.button ? form.application_form.button.url : '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Links Section -->
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Links</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="applicationFormLinks">
|
||||
<% (form.application_form && form.application_form.links || []).forEach((link, index) => { %>
|
||||
<div class="mb-3 border p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Text</label>
|
||||
<input type="text" class="form-control" name="applicationFormLinkText_<%= index %>" value="<%= link.text %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">URL</label>
|
||||
<input type="text" class="form-control" name="applicationFormLinkUrl_<%= index %>" value="<%= link.url %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fixed Bottom Buttons -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary">
|
||||
<i class="fas fa-undo"></i>
|
||||
<span>Reset</span>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i>
|
||||
<span>Save Changes</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Thêm input file ẩn cho upload ảnh -->
|
||||
<input type="file" id="directImageUpload" style="display: none;">
|
||||
<input type="hidden" id="currentImageType" name="imageType">
|
||||
<input type="hidden" id="currentTargetInput" name="targetInput">
|
||||
|
||||
<script>
|
||||
let originalFormData = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Lưu trữ dữ liệu gốc để so sánh thay đổi
|
||||
originalFormData = <%- JSON.stringify(form) %>;
|
||||
|
||||
// Khởi tạo form handlers
|
||||
initializeFormHandlers();
|
||||
});
|
||||
|
||||
function initializeFormHandlers() {
|
||||
// Form submission
|
||||
const form = document.querySelector('form');
|
||||
form.addEventListener('submit', handleFormSubmit);
|
||||
|
||||
// Upload ảnh
|
||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
const imageType = this.dataset.imageType || 'academics';
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
});
|
||||
|
||||
// Khởi tạo direct image upload
|
||||
initializeDirectImageUpload();
|
||||
}
|
||||
|
||||
function initializeDirectImageUpload() {
|
||||
const fileInput = document.getElementById('directImageUpload');
|
||||
fileInput.addEventListener('change', handleDirectImageUpload);
|
||||
}
|
||||
|
||||
function openImageUploader(targetInput, imageType) {
|
||||
// Lưu thông tin upload hiện tại
|
||||
document.getElementById('currentImageType').value = imageType;
|
||||
document.getElementById('currentTargetInput').value = targetInput;
|
||||
|
||||
// Kích hoạt input file
|
||||
const fileInput = document.getElementById('directImageUpload');
|
||||
fileInput.click();
|
||||
}
|
||||
|
||||
async function handleDirectImageUpload(e) {
|
||||
if (!this.files || !this.files[0]) return;
|
||||
|
||||
const file = this.files[0];
|
||||
const imageType = document.getElementById('currentImageType').value;
|
||||
const targetInput = document.getElementById('currentTargetInput').value;
|
||||
|
||||
try {
|
||||
// Disable nút upload và hiển thị loading
|
||||
const uploadButton = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
if (uploadButton) {
|
||||
uploadButton.disabled = true;
|
||||
uploadButton.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
|
||||
}
|
||||
|
||||
// Tạo form data
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
// Upload ảnh
|
||||
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.path) {
|
||||
// Tìm input cần cập nhật, ưu tiên theo name, nếu không có thì tìm theo id
|
||||
const inputElement = document.querySelector(`[name="${targetInput}"]`) || document.getElementById(targetInput);
|
||||
if (inputElement) {
|
||||
inputElement.value = result.path;
|
||||
// Cập nhật preview
|
||||
updateImagePreview(inputElement, result.path);
|
||||
}
|
||||
|
||||
showToast('Success', 'Image uploaded successfully', 'success');
|
||||
} else {
|
||||
throw new Error(result.error || 'Upload failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
showToast('Error', 'Failed to upload image: ' + error.message, 'error');
|
||||
} finally {
|
||||
// Reset nút upload
|
||||
const uploadButton = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
if (uploadButton) {
|
||||
uploadButton.disabled = false;
|
||||
uploadButton.innerHTML = '<i class="fas fa-upload me-1"></i>Upload';
|
||||
}
|
||||
// Reset file input
|
||||
this.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function updateImagePreview(inputElement, imagePath) {
|
||||
// Tìm hoặc tạo phần tử preview theo cách giống như Partnerships
|
||||
let imgPreview = inputElement.parentElement.nextElementSibling;
|
||||
while (imgPreview && !imgPreview.classList.contains('mt-2')) {
|
||||
imgPreview = imgPreview.nextElementSibling;
|
||||
}
|
||||
|
||||
if (!imgPreview) {
|
||||
// Tạo mới phần tử preview nếu chưa có
|
||||
imgPreview = document.createElement('div');
|
||||
imgPreview.className = 'mt-2';
|
||||
const img = document.createElement('img');
|
||||
img.className = 'img-thumbnail';
|
||||
|
||||
// Set style dựa vào loại ảnh
|
||||
const targetInput = inputElement.name || inputElement.id;
|
||||
if (targetInput.toLowerCase().includes('logo')) {
|
||||
img.style.height = '100px';
|
||||
img.style.objectFit = 'contain';
|
||||
} else {
|
||||
img.style.height = '200px';
|
||||
img.style.width = '100%';
|
||||
img.style.objectFit = 'cover';
|
||||
}
|
||||
|
||||
img.alt = 'Image preview';
|
||||
imgPreview.appendChild(img);
|
||||
inputElement.parentElement.parentElement.appendChild(imgPreview);
|
||||
}
|
||||
|
||||
// Cập nhật đường dẫn hình ảnh
|
||||
const img = imgPreview.querySelector('img');
|
||||
if (img) {
|
||||
img.src = imagePath;
|
||||
}
|
||||
}
|
||||
|
||||
function handleFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
console.log('=== Form Submit Debug ===');
|
||||
|
||||
// Chuẩn bị dữ liệu form
|
||||
const formData = prepareFormData();
|
||||
console.log('Complete form data:', formData);
|
||||
|
||||
// Log từng section
|
||||
Object.keys(formData).forEach(key => {
|
||||
console.log(`${key} data:`, formData[key]);
|
||||
});
|
||||
|
||||
// Validate dữ liệu
|
||||
const errors = validateFormData(formData);
|
||||
if (errors.length > 0) {
|
||||
console.log('Validation errors:', errors);
|
||||
showToast('Error', errors.join('. '), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Log hidden inputs trước khi cập nhật
|
||||
Object.keys(formData).forEach(key => {
|
||||
const input = document.getElementById(`${key}Json`);
|
||||
console.log(`Hidden input ${key}Json:`, {
|
||||
element: input,
|
||||
value: input?.value
|
||||
});
|
||||
});
|
||||
|
||||
// Cập nhật hidden inputs (map key -> input id)
|
||||
const keyToInputId = {
|
||||
admission: 'admissionJson',
|
||||
apply: 'applyJson',
|
||||
application_form: 'applicationFormJson'
|
||||
};
|
||||
|
||||
Object.keys(formData).forEach(key => {
|
||||
const inputId = keyToInputId[key] || `${key}Json`;
|
||||
const input = document.getElementById(inputId);
|
||||
if (input) {
|
||||
const jsonValue = JSON.stringify(formData[key]);
|
||||
input.value = jsonValue;
|
||||
console.log(`Updated ${inputId} with:`, jsonValue);
|
||||
} else {
|
||||
console.warn(`Hidden input not found: ${inputId}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Thông báo đang lưu
|
||||
showToast('Info', 'Saving changes...', 'info');
|
||||
|
||||
// Log form final state
|
||||
console.log('Form final state:', {
|
||||
admissionJson: document.getElementById('admissionJson')?.value,
|
||||
applyJson: document.getElementById('applyJson')?.value,
|
||||
applicationFormJson: document.getElementById('applicationFormJson')?.value
|
||||
});
|
||||
|
||||
// Submit form
|
||||
this.submit();
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
console.error('Error stack:', error.stack);
|
||||
showToast('Error', 'Error processing form: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function prepareFormData() {
|
||||
return {
|
||||
admission: prepareAdmissionData(),
|
||||
apply: prepareApplyData(),
|
||||
application_form: prepareApplicationFormData()
|
||||
};
|
||||
}
|
||||
|
||||
function prepareAdmissionData() {
|
||||
try {
|
||||
// Basic info
|
||||
const data = {
|
||||
background_image: document.getElementById('admissionBgImage')?.value || '',
|
||||
title: document.getElementById('admissionTitle')?.value || '',
|
||||
year: document.getElementById('admissionYear')?.value || '',
|
||||
description: document.getElementById('admissionDescription')?.value || '',
|
||||
form: {
|
||||
fields: [],
|
||||
button: {
|
||||
text: document.getElementById('admissionFormButton')?.value || '',
|
||||
url: document.getElementById('admissionFormButtonUrl')?.value || '#'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Form fields
|
||||
const formFields = document.querySelectorAll('#admissionFormFields > div');
|
||||
if (formFields) {
|
||||
data.form.fields = Array.from(formFields).map(field => {
|
||||
const inputs = field.querySelectorAll('input');
|
||||
return {
|
||||
type: inputs[0]?.value || '',
|
||||
placeholder: inputs[1]?.value || ''
|
||||
};
|
||||
}).filter(field => field.type || field.placeholder);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error preparing admission data:', error);
|
||||
return {
|
||||
background_image: '',
|
||||
title: '',
|
||||
year: '',
|
||||
description: '',
|
||||
form: {
|
||||
fields: [],
|
||||
button: {
|
||||
text: '',
|
||||
url: '#'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function prepareApplyData() {
|
||||
try {
|
||||
const data = {
|
||||
title: document.getElementById('applyTitle')?.value || '',
|
||||
steps: []
|
||||
};
|
||||
|
||||
// Steps
|
||||
const steps = document.querySelectorAll('#applySteps > div');
|
||||
if (steps) {
|
||||
data.steps = Array.from(steps).map(step => ({
|
||||
title: step.querySelector('input[name^="applyStepTitle_"]')?.value || '',
|
||||
description: step.querySelector('textarea[name^="applyStepDescription_"]')?.value || ''
|
||||
})).filter(step => step.title || step.description);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error preparing apply data:', error);
|
||||
return {
|
||||
title: '',
|
||||
steps: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function prepareApplicationFormData() {
|
||||
try {
|
||||
console.log('=== Debug Application Form Data ===');
|
||||
|
||||
// Log các elements
|
||||
console.log('Title element:', document.getElementById('applicationFormTitle'));
|
||||
console.log('Question element:', document.getElementById('applicationFormQuestion'));
|
||||
console.log('Button text element:', document.getElementById('applicationFormButtonText'));
|
||||
console.log('Button icon element:', document.getElementById('applicationFormButtonIcon'));
|
||||
|
||||
const data = {
|
||||
title: document.getElementById('applicationFormTitle')?.value || '',
|
||||
question: document.getElementById('applicationFormQuestion')?.value || '',
|
||||
button: {
|
||||
text: document.getElementById('applicationFormButtonText')?.value || '',
|
||||
icon: document.getElementById('applicationFormButtonIcon')?.value || '',
|
||||
url: document.getElementById('applicationFormButtonUrl')?.value || '#'
|
||||
},
|
||||
links: []
|
||||
};
|
||||
|
||||
// Log giá trị cơ bản
|
||||
console.log('Basic data:', {
|
||||
title: data.title,
|
||||
question: data.question,
|
||||
button: data.button
|
||||
});
|
||||
|
||||
// Debug links
|
||||
const linksContainer = document.getElementById('applicationFormLinks');
|
||||
console.log('Links container:', linksContainer);
|
||||
|
||||
const linkDivs = document.querySelectorAll('#applicationFormLinks > div');
|
||||
console.log('Found link divs:', linkDivs.length);
|
||||
|
||||
if (linkDivs.length > 0) {
|
||||
data.links = Array.from(linkDivs).map((link, index) => {
|
||||
const textInput = link.querySelector('input[name^="applicationFormLinkText_"]');
|
||||
const urlInput = link.querySelector('input[name^="applicationFormLinkUrl_"]');
|
||||
|
||||
return {
|
||||
text: textInput?.value || '',
|
||||
url: urlInput?.value || ''
|
||||
};
|
||||
}).filter(link => link.text || link.url);
|
||||
}
|
||||
|
||||
console.log('Final application form data:', data);
|
||||
return data;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error preparing application form data:', error);
|
||||
console.error('Error stack:', error.stack);
|
||||
return {
|
||||
title: '',
|
||||
question: '',
|
||||
button: {
|
||||
text: '',
|
||||
icon: '',
|
||||
url: '#'
|
||||
},
|
||||
links: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function validateFormData(data) {
|
||||
const errors = [];
|
||||
|
||||
// Validate admission
|
||||
if (!data.admission.title) errors.push('Admission title is required');
|
||||
if (!data.admission.year) errors.push('Admission year is required');
|
||||
|
||||
// Validate apply
|
||||
if (!data.apply.title) errors.push('Apply title is required');
|
||||
if (!Array.isArray(data.apply.steps)) errors.push('Apply steps must be an array');
|
||||
|
||||
// Validate application form
|
||||
if (!data.application_form.title) errors.push('Application form title is required');
|
||||
if (!data.application_form.question) errors.push('Application form question is required');
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function showToast(title, message, type = 'info') {
|
||||
if (window.toastManager) {
|
||||
window.toastManager[type](message);
|
||||
return;
|
||||
}
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast align-items-center text-white bg-${type === 'error' ? 'danger' : type} border-0`;
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.setAttribute('aria-live', 'assertive');
|
||||
toast.setAttribute('aria-atomic', 'true');
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<strong>${title}:</strong> ${message}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add toast to container
|
||||
let container = document.querySelector('.toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.className = 'toast-container position-fixed top-0 end-0 p-3';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
container.appendChild(toast);
|
||||
|
||||
// Show toast
|
||||
const bsToast = new bootstrap.Toast(toast, {
|
||||
animation: true,
|
||||
autohide: true,
|
||||
delay: 3000
|
||||
});
|
||||
bsToast.show();
|
||||
|
||||
// Remove toast after hidden
|
||||
toast.addEventListener('hidden.bs.toast', () => {
|
||||
toast.remove();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,355 +0,0 @@
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-sitemap me-2"></i>Menu Structure
|
||||
</h6>
|
||||
<button class="btn btn-primary btn-sm" onclick="prepareAddMenu()">
|
||||
<i class="fas fa-plus me-1"></i>Add Root Menu
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="nestedMenuContainer" class="nested-menu-container">
|
||||
<ul id="menuRoot" class="list-unstyled menu-group" data-id="root">
|
||||
<% function renderMenu(items) { %>
|
||||
<% items.forEach(item => { %>
|
||||
<li class="menu-item-wrapper mb-2" data-id="<%= item._id %>">
|
||||
<div class="menu-item-row d-flex align-items-center p-3 rounded border hover-shadow-sm bg-white">
|
||||
<div class="menu-drag-handle me-2 text-muted">
|
||||
<i class="fas fa-grip-vertical"></i>
|
||||
</div>
|
||||
|
||||
<div class="menu-toggle-icon me-2">
|
||||
<% if (item.children && item.children.length > 0) { %>
|
||||
<button class="btn btn-sm p-0 btn-toggle-nested" type="button">
|
||||
<i class="fas fa-chevron-down transition-base text-muted" style="width: 12px;"></i>
|
||||
</button>
|
||||
<% } else { %>
|
||||
<div style="width: 24px;"></div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="menu-icon me-3">
|
||||
<i class="fas <%= (item.type === 'external' ? 'fa-external-link-alt text-info' : 'fa-link text-secondary') %>"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="fw-bold text-dark"><%= item.title %></span>
|
||||
<% if (item.type === 'external') { %>
|
||||
<span class="badge bg-light text-dark border ms-2" style="font-size: 0.7rem;">External</span>
|
||||
<% } %>
|
||||
<% if (item.status === 'inactive') { %>
|
||||
<span class="badge ms-2 bg-soft-danger text-danger">Inactive</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-soft-success ms-2">Active</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="text-muted small text-truncate" style="max-width: 300px;">
|
||||
<i class="fas fa-link me-1" style="font-size: 0.75rem;"></i><%= item.url %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-actions">
|
||||
<div class="btn-group-action">
|
||||
<button class="btn btn-sm btn-add-child" data-id="<%= item._id %>" title="Add Sub-menu">
|
||||
<i class="fas fa-plus text-action-add"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-edit-menu" data-item='<%= JSON.stringify(item).replace(/'/g, "'") %>' title="Edit">
|
||||
<i class="fas fa-edit text-action-edit"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-delete-menu" data-id="<%= item._id %>" data-title="<%= item.title %>" title="Delete">
|
||||
<i class="fas fa-trash text-action-delete"></i>
|
||||
</button>
|
||||
</div>
|
||||
<form id="delete-form-<%= item._id %>" action="/admin/header/menu/delete" method="POST" class="d-none">
|
||||
<input type="hidden" name="id" value="<%= item._id %>">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="list-unstyled menu-group mt-2 ps-4 ms-2 border-start nested-list" style="min-height: 5px;" data-id="<%= item._id %>">
|
||||
<% if (item.children && item.children.length > 0) { %>
|
||||
<% renderMenu(item.children) %>
|
||||
<% } %>
|
||||
</ul>
|
||||
</li>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
<% if (menuData.tree && menuData.tree.length > 0) { %>
|
||||
<% renderMenu(menuData.tree) %>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% if (!menuData.tree || menuData.tree.length === 0) { %>
|
||||
<div class="text-center py-5">
|
||||
<img src="/assets/img/icon/empty-state.svg" alt="Empty" style="width: 120px; opacity: 0.5;" class="mb-3">
|
||||
<p class="text-muted">No menu items found. Start by adding a root menu.</p>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Menu Tab JS Initialized');
|
||||
initSortable();
|
||||
|
||||
function initSortable() {
|
||||
const menuGroups = document.querySelectorAll('.menu-group');
|
||||
menuGroups.forEach(group => {
|
||||
new Sortable(group, {
|
||||
group: 'nested-menu',
|
||||
animation: 150,
|
||||
fallbackOnBody: true,
|
||||
swapThreshold: 0.65,
|
||||
handle: '.menu-drag-handle',
|
||||
ghostClass: 'menu-ghost',
|
||||
chosenClass: 'menu-chosen',
|
||||
dragClass: 'menu-dragging',
|
||||
onEnd: function (evt) {
|
||||
console.log('Drag ended', evt);
|
||||
// Highlight that changes need saving
|
||||
const saveBtn = document.getElementById('saveHeaderBtn');
|
||||
if (saveBtn && typeof window.markHeaderChanged === 'function') {
|
||||
window.markHeaderChanged();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Bind Edit/Add/Delete buttons
|
||||
bindMenuActions();
|
||||
|
||||
function bindMenuActions() {
|
||||
// Use event delegation for better performance and to handle re-rendered items if any
|
||||
const container = document.getElementById('nestedMenuContainer');
|
||||
if (container) {
|
||||
container.addEventListener('click', function(e) {
|
||||
const editBtn = e.target.closest('.btn-edit-menu');
|
||||
if (editBtn) {
|
||||
try {
|
||||
const item = JSON.parse(editBtn.dataset.item);
|
||||
console.log('=== TRACE: Edit Menu Clicked ===', item);
|
||||
prepareEditMenu(item);
|
||||
} catch (e) {
|
||||
console.error('Error parsing menu item data:', e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const addChildBtn = e.target.closest('.btn-add-child');
|
||||
if (addChildBtn) {
|
||||
const pid = addChildBtn.dataset.id;
|
||||
console.log('=== TRACE: Add Child Clicked ===', { pid });
|
||||
if (typeof prepareAddChild === 'function') {
|
||||
prepareAddChild(pid);
|
||||
} else {
|
||||
console.error('prepareAddChild function not found');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteBtn = e.target.closest('.btn-delete-menu');
|
||||
if (deleteBtn) {
|
||||
const id = deleteBtn.dataset.id;
|
||||
const title = deleteBtn.dataset.title;
|
||||
console.log('=== TRACE: Delete Menu Clicked ===', { id, title });
|
||||
if (confirm(`Are you sure you want to delete "${title}" and all its sub-menu items?`)) {
|
||||
const form = document.getElementById('delete-form-' + id);
|
||||
console.log('=== TRACE: Submitting Delete Form ===', form ? form.action : 'FORM NOT FOUND');
|
||||
if (form) form.submit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const toggleBtn = e.target.closest('.btn-toggle-nested');
|
||||
if (toggleBtn) {
|
||||
const wrapper = toggleBtn.closest('.menu-item-wrapper');
|
||||
const nestedUl = wrapper.querySelector('.nested-list');
|
||||
const icon = toggleBtn.querySelector('i');
|
||||
|
||||
if (nestedUl) {
|
||||
const isCollapsed = nestedUl.classList.toggle('collapsed');
|
||||
if (isCollapsed) {
|
||||
icon.style.transform = 'rotate(-90deg)';
|
||||
} else {
|
||||
icon.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function prepareAddMenu() {
|
||||
console.log('=== TRACE: prepareAddMenu Called ===');
|
||||
const form = document.getElementById('menuForm');
|
||||
if (!form) return;
|
||||
form.action = '/admin/header/menu/create';
|
||||
document.getElementById('modalTitle').innerText = 'Add Root Menu';
|
||||
document.getElementById('menuId').value = '';
|
||||
document.getElementById('parentId').value = '';
|
||||
document.getElementById('formTitle').value = '';
|
||||
document.getElementById('formUrl').value = '';
|
||||
document.getElementById('formOrder').value = '0';
|
||||
document.getElementById('formStatus').value = 'active';
|
||||
document.getElementById('typeInternal').checked = true;
|
||||
|
||||
const modalElement = document.getElementById('modalAddMenu');
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(modalElement);
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function prepareAddChild(parentId) {
|
||||
prepareAddMenu();
|
||||
document.getElementById('parentId').value = parentId;
|
||||
document.getElementById('modalTitle').innerText = 'Add Sub-Menu Item';
|
||||
}
|
||||
|
||||
function prepareEditMenu(item) {
|
||||
const form = document.getElementById('menuForm');
|
||||
if (!form) return;
|
||||
form.action = '/admin/header/menu/update/' + item._id;
|
||||
document.getElementById('modalTitle').innerText = 'Edit Menu Item';
|
||||
document.getElementById('menuId').value = item._id;
|
||||
document.getElementById('parentId').value = item.parentId || '';
|
||||
document.getElementById('formTitle').value = item.title;
|
||||
document.getElementById('formUrl').value = item.url;
|
||||
document.getElementById('formOrder').value = item.order;
|
||||
document.getElementById('formStatus').value = item.status;
|
||||
|
||||
if (item.type === 'external') {
|
||||
document.getElementById('typeExternal').checked = true;
|
||||
} else {
|
||||
document.getElementById('typeInternal').checked = true;
|
||||
}
|
||||
|
||||
const modalElement = document.getElementById('modalAddMenu');
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(modalElement);
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function collectMenuData() {
|
||||
const items = [];
|
||||
function traverse(element, parentId = null) {
|
||||
const children = element.children;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const li = children[i];
|
||||
if (li.tagName !== 'LI') continue;
|
||||
|
||||
const id = li.dataset.id;
|
||||
if (!id) continue;
|
||||
|
||||
items.push({
|
||||
id: id,
|
||||
order: i + 1,
|
||||
parentId: parentId === 'root' ? null : parentId
|
||||
});
|
||||
|
||||
const subUl = li.querySelector('.menu-group');
|
||||
if (subUl) {
|
||||
traverse(subUl, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
const rootUl = document.getElementById('menuRoot');
|
||||
if (rootUl) traverse(rootUl, 'root');
|
||||
return items;
|
||||
}
|
||||
|
||||
window.saveMenuChanges = function(showToastFlag = true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('=== TRACE: saveMenuChanges Called (Reorder) ===');
|
||||
const items = collectMenuData();
|
||||
|
||||
if (items.length === 0) {
|
||||
console.warn('No menu items found to reorder');
|
||||
return resolve({ success: true, message: 'No items' });
|
||||
}
|
||||
|
||||
const saveBtn = document.getElementById('saveHeaderBtn');
|
||||
const originalHtml = saveBtn ? saveBtn.innerHTML : '';
|
||||
|
||||
// Only manage button state if this is a direct call (not unified save)
|
||||
const manageButton = showToastFlag && saveBtn;
|
||||
|
||||
if (manageButton) {
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Saving Menu...';
|
||||
}
|
||||
|
||||
fetch('/admin/header/menu/reorder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ items: items })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
if (showToastFlag) showNotification('Menu structure saved', 'success');
|
||||
resolve(data);
|
||||
} else {
|
||||
reject(new Error(data.message));
|
||||
}
|
||||
})
|
||||
.catch(error => reject(error))
|
||||
.finally(() => {
|
||||
if (manageButton) {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = originalHtml;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.collectMenuData = collectMenuData;
|
||||
|
||||
// Global reset fallback
|
||||
window.prepareAddMenu = prepareAddMenu;
|
||||
</script>
|
||||
|
||||
<!-- Redundant buttons removed to use global buttons in index.ejs -->
|
||||
|
||||
<style>
|
||||
.nested-menu-container {
|
||||
padding: var(--spacing-3);
|
||||
}
|
||||
.menu-group {
|
||||
min-height: 10px;
|
||||
}
|
||||
.menu-item-row {
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
.menu-drag-handle { cursor: grab; padding: 5px; }
|
||||
.menu-drag-handle:active { cursor: grabbing; }
|
||||
|
||||
/* SortableJS Classes mapped to our variables */
|
||||
.menu-ghost {
|
||||
opacity: 0.4;
|
||||
border: 2px dashed var(--primary-color) !important;
|
||||
}
|
||||
.menu-chosen {
|
||||
background-color: var(--primary-soft) !important;
|
||||
}
|
||||
.menu-dragging {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Collapse/Expand Styles */
|
||||
.nested-list {
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
max-height: 2000px; /* Large enough for nested items */
|
||||
}
|
||||
.nested-list.collapsed {
|
||||
max-height: 0;
|
||||
margin-top: 0 !important;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.btn-toggle-nested i {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.transition-base {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
@@ -1,216 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
|
||||
<%= title %>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Manage FAQ section content</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="<%= frontendUrl %>/" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-2"></i>View Homepage
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form method="POST" class="content-with-fixed-buttons" id="faqForm" action="/admin/faq/update">
|
||||
<!-- Hidden input for items JSON data -->
|
||||
<input type="hidden" name="items" id="itemsJson">
|
||||
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0"><i class="fas fa-question-circle me-2"></i>FAQ Settings</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- Header Section -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input type="text" class="form-control" name="heading" id="heading"
|
||||
value="<%= data.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subheading</label>
|
||||
<input type="text" class="form-control" name="subheading" id="subheading"
|
||||
value="<%= data.subheading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" name="description" id="description"
|
||||
rows="2"><%= data.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA Button Section -->
|
||||
<h6 class="fw-medium mb-3">CTA Button</h6>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Button Label</label>
|
||||
<input type="text" class="form-control" name="ctaLabel"
|
||||
value="<%= data.ctaButton && data.ctaButton.label ? data.ctaButton.label : '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Button Link</label>
|
||||
<input type="text" class="form-control" name="ctaHref"
|
||||
value="<%= data.ctaButton && data.ctaButton.href ? data.ctaButton.href : '' %>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<!-- FAQ Items -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-medium mb-0">Frequently Asked Questions</h6>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="addFaqItem()">
|
||||
<i class="fas fa-plus"></i> Add Question
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="faqItemsContainer">
|
||||
<% if (data.items && data.items.length> 0) { %>
|
||||
<% data.items.forEach((item, index)=> { %>
|
||||
<div class="card mb-3 faq-item">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Question</label>
|
||||
<input type="text" class="form-control"
|
||||
name="itemQuestion_<%= index %>"
|
||||
value="<%= item.question || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Answer</label>
|
||||
<textarea class="form-control" name="itemAnswer_<%= index %>"
|
||||
rows="3"><%= item.answer || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm mt-3"
|
||||
onclick="removeFaqItem(this)">
|
||||
<i class="fas fa-trash me-2"></i>Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fixed Bottom Buttons -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
|
||||
<i class="fas fa-undo me-2"></i>Reset
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<i class="fas fa-save me-2"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="application/json" id="faqDataJson"><%- JSON.stringify(data) %></script>
|
||||
|
||||
<script>
|
||||
let originalFormData = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
try {
|
||||
var jsonScript = document.getElementById('faqDataJson');
|
||||
originalFormData = JSON.parse(jsonScript.textContent);
|
||||
} catch (e) {
|
||||
console.error('Error parsing originalFormData:', e);
|
||||
originalFormData = { items: [] };
|
||||
}
|
||||
|
||||
initializeFormHandlers();
|
||||
});
|
||||
|
||||
function initializeFormHandlers() {
|
||||
const form = document.getElementById('faqForm');
|
||||
form.addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
|
||||
|
||||
try {
|
||||
updateItemsJson();
|
||||
this.submit();
|
||||
} catch (error) {
|
||||
console.error('Error updating data:', error);
|
||||
alert('Failed to process form data. Please try again.');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateItemsJson() {
|
||||
const items = [];
|
||||
document.querySelectorAll('.faq-item').forEach((item, index) => {
|
||||
const question = item.querySelector(`[name="itemQuestion_${index}"]`)?.value ||
|
||||
item.querySelector('[name^="itemQuestion_"]')?.value || '';
|
||||
const answer = item.querySelector(`[name="itemAnswer_${index}"]`)?.value ||
|
||||
item.querySelector('[name^="itemAnswer_"]')?.value || '';
|
||||
|
||||
items.push({ question, answer });
|
||||
});
|
||||
|
||||
document.getElementById('itemsJson').value = JSON.stringify(items);
|
||||
}
|
||||
|
||||
function addFaqItem() {
|
||||
const container = document.getElementById('faqItemsContainer');
|
||||
const index = container.querySelectorAll('.faq-item').length;
|
||||
|
||||
const html = `
|
||||
<div class="card mb-3 faq-item">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Question</label>
|
||||
<input type="text" class="form-control" name="itemQuestion_${index}" value="">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Answer</label>
|
||||
<textarea class="form-control" name="itemAnswer_${index}" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm mt-3"
|
||||
onclick="removeFaqItem(this)">
|
||||
<i class="fas fa-trash me-2"></i>Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function removeFaqItem(button) {
|
||||
if (confirm('Are you sure you want to remove this question?')) {
|
||||
button.closest('.faq-item').remove();
|
||||
reindexItems();
|
||||
}
|
||||
}
|
||||
|
||||
function reindexItems() {
|
||||
document.querySelectorAll('.faq-item').forEach((item, index) => {
|
||||
item.querySelectorAll('[name^="item"]').forEach(input => {
|
||||
const baseName = input.name.replace(/_\d+$/, '');
|
||||
input.name = `${baseName}_${index}`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
if (confirm('Are you sure you want to reset all changes?')) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,231 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark)">
|
||||
Homepage Management
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Edit content displayed on homepage</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="<%= frontendUrl %>" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-2"></i>View Homepage
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form action="/admin/home/update" method="POST" class="content-with-fixed-buttons">
|
||||
<!-- Hidden inputs for JSON data -->
|
||||
<input type="hidden" name="hero" id="heroJson" />
|
||||
<input type="hidden" name="whyChooseUs" id="whyChooseUsJson" />
|
||||
<input type="hidden" name="visaSolutions" id="visaSolutionsJson" />
|
||||
<input type="hidden" name="visaCountries" id="visaCountriesJson" />
|
||||
<input type="hidden" name="testimonials" id="testimonialsJson" />
|
||||
<input type="hidden" name="videoGallery" id="videoGalleryJson" />
|
||||
<input type="hidden" name="faq" id="faqJson" />
|
||||
<input type="hidden" name="achievements" id="achievementsJson" />
|
||||
<input type="hidden" name="partners" id="partnersJson" />
|
||||
<input type="hidden" name="blogPreview" id="blogPreviewJson" />
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#hero" role="tab">
|
||||
<i class="fas fa-home me-2"></i>Hero
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#whychooseus" role="tab">
|
||||
<i class="fas fa-star me-2"></i>Why Choose Us
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#visasolutions" role="tab">
|
||||
<i class="fas fa-concierge-bell me-2"></i>Visa Solutions
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#visacountries" role="tab">
|
||||
<i class="fas fa-globe-americas me-2"></i>Visa Countries
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#testimonials" role="tab">
|
||||
<i class="fas fa-comments me-2"></i>Testimonials
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#videogallery" role="tab">
|
||||
<i class="fas fa-video me-2"></i>Video Gallery
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#faq" role="tab">
|
||||
<i class="fas fa-question-circle me-2"></i>FAQ
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#achievements" role="tab">
|
||||
<i class="fas fa-chart-pie me-2"></i>Achievements
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#partners" role="tab">
|
||||
<i class="fas fa-handshake me-2"></i>Partners
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#blogpreview" role="tab">
|
||||
<i class="fas fa-blog me-2"></i>Blog Preview
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<%- include('sections/hero') %>
|
||||
<%- include('sections/whyChooseUs') %>
|
||||
<%- include('sections/visaSolutions') %>
|
||||
<%- include('sections/visaCountries') %>
|
||||
<%- include('sections/testimonials') %>
|
||||
<%- include('sections/videoGallery') %>
|
||||
<%- include('sections/faq') %>
|
||||
<%- include('sections/achievements') %>
|
||||
<%- include('sections/partners') %>
|
||||
<%- include('sections/blogPreview') %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Move buttons to fixed bottom -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary">
|
||||
<i class="fas fa-undo"></i>
|
||||
<span>Reset</span>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i>
|
||||
<span>Save Changes</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image upload input -->
|
||||
<input type="file" id="directImageUpload" style="display: none" />
|
||||
<input type="hidden" id="currentImageType" name="imageType" />
|
||||
<input type="hidden" id="currentTargetInput" name="targetInput" />
|
||||
|
||||
<script>
|
||||
/**
|
||||
* BRIDGE SCRIPT: Cho phép các section lẻ tự đăng ký logic lấy dữ liệu.
|
||||
* Cách dùng trong file lẻ (vị dụ hero.ejs):
|
||||
* <script>
|
||||
* window.homeScrapers = window.homeScrapers || {};
|
||||
* window.homeScrapers.hero = () => ({ title: document.getElementById('heroTitle').value, ... });
|
||||
* <\/script>
|
||||
*/
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const form = document.querySelector("form");
|
||||
if (form) {
|
||||
form.addEventListener("submit", function (e) {
|
||||
console.log("Form submitting, collecting data from scrapers...");
|
||||
|
||||
// Tự động thu gom dữ liệu từ các section đã đăng ký
|
||||
Object.keys(window.homeScrapers).forEach(section => {
|
||||
const input = document.getElementById(section + 'Json');
|
||||
if (input) {
|
||||
try {
|
||||
const data = window.homeScrapers[section]();
|
||||
console.log(`- Collected data for [${section}]:`, data);
|
||||
input.value = JSON.stringify(data);
|
||||
} catch (err) {
|
||||
console.error(`Error scraping section [${section}]:`, err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Để form tự submit tự nhiên sau khi đã điền xong các hidden inputs
|
||||
});
|
||||
}
|
||||
|
||||
// Khởi tạo các nút upload ảnh (dùng chung cho toàn bộ các section)
|
||||
initImageUploads();
|
||||
});
|
||||
|
||||
// --- UTILITIES (Dùng chung) ---
|
||||
|
||||
function initImageUploads() {
|
||||
document.addEventListener("click", function (e) {
|
||||
const btn = e.target.closest(".btn-upload-image");
|
||||
if (btn) {
|
||||
document.getElementById("currentImageType").value = btn.dataset.imageType;
|
||||
document.getElementById("currentTargetInput").value = btn.dataset.targetInput;
|
||||
document.getElementById("directImageUpload").click();
|
||||
}
|
||||
});
|
||||
|
||||
const fileInput = document.getElementById("directImageUpload");
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener("change", handleDirectImageUpload);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDirectImageUpload(e) {
|
||||
if (!this.files || !this.files[0]) return;
|
||||
const file = this.files[0];
|
||||
const imageType = document.getElementById("currentImageType").value;
|
||||
const targetInput = document.getElementById("currentTargetInput").value;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("image", file);
|
||||
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, { method: "POST", body: formData });
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.path) {
|
||||
const input = document.getElementById(targetInput);
|
||||
if (input) {
|
||||
input.value = result.path;
|
||||
// Cập nhật preview nếu có img ngay sau input group
|
||||
const previewImg = input.closest('.input-group')?.nextElementSibling?.querySelector('img');
|
||||
if (previewImg) {
|
||||
previewImg.src = result.path;
|
||||
previewImg.classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
showToast("Success", "Image uploaded successfully", "success");
|
||||
} else {
|
||||
throw new Error(result.error || "Upload failed");
|
||||
}
|
||||
} catch (error) {
|
||||
showToast("Error", "Upload failed: " + error.message, "error");
|
||||
}
|
||||
this.value = "";
|
||||
}
|
||||
|
||||
function showToast(title, message, type = "info") {
|
||||
let container = document.querySelector(".toast-container") || (() => {
|
||||
const c = document.createElement("div");
|
||||
c.className = "toast-container position-fixed top-0 end-0 p-3";
|
||||
document.body.appendChild(c);
|
||||
return c;
|
||||
})();
|
||||
|
||||
const toast = document.createElement("div");
|
||||
toast.className = `toast align-items-center text-white bg-${type === "error" ? "danger" : type} border-0`;
|
||||
toast.setAttribute("role", "alert");
|
||||
toast.innerHTML = `<div class="d-flex"><div class="toast-body"><strong>${title}:</strong> ${message}</div><button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
|
||||
container.appendChild(toast);
|
||||
new bootstrap.Toast(toast, { autohide: true, delay: 3000 }).show();
|
||||
toast.addEventListener("hidden.bs.toast", () => toast.remove());
|
||||
}
|
||||
</script>
|
||||
@@ -1,122 +0,0 @@
|
||||
<!-- Achievements Tab -->
|
||||
<div class="tab-pane fade" id="achievements" role="tabpanel">
|
||||
<div class="row g-4">
|
||||
<!-- Basic Info -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-chart-pie me-2"></i>General Information
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="achievementsHeading"
|
||||
value="<%= data.achievements?.heading || '' %>"
|
||||
placeholder="e.g., Our Achievements in Numbers"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subheading</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="achievementsSubheading"
|
||||
value="<%= data.achievements?.subheading || '' %>"
|
||||
placeholder="e.g., Did You Know"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Achievement Items -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-list-ul me-2"></i>Achievement Items (Fixed 4 Items)
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body" id="achievementItemsContainer">
|
||||
<% for(let i=0; i<4; i++) {
|
||||
const item = (data.achievements?.items && data.achievements.items[i]) || {};
|
||||
%>
|
||||
<div class="card mb-3 bg-light border achievement-item">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="card-title fw-bold mb-0">Achievement #<%= i + 1 %></h6>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-medium">Value (Number)</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control achievement-value"
|
||||
value="<%= item.value || '' %>"
|
||||
placeholder="e.g., 95"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-medium">Suffix</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control achievement-suffix"
|
||||
value="<%= item.suffix || '' %>"
|
||||
placeholder="e.g., %"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Label</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control achievement-label"
|
||||
value="<%= item.label || '' %>"
|
||||
placeholder="e.g., Visa Success Rate"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea
|
||||
class="form-control achievement-description"
|
||||
rows="2"
|
||||
placeholder="Short description of this achievement"
|
||||
><%= item.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Đăng ký scraper cho phần achievements
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
window.homeScrapers.achievements = function() {
|
||||
const items = [];
|
||||
document.querySelectorAll('.achievement-item').forEach(el => {
|
||||
items.push({
|
||||
value: el.querySelector('.achievement-value').value,
|
||||
suffix: el.querySelector('.achievement-suffix').value,
|
||||
label: el.querySelector('.achievement-label').value,
|
||||
description: el.querySelector('.achievement-description').value
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
heading: document.getElementById('achievementsHeading').value,
|
||||
subheading: document.getElementById('achievementsSubheading').value,
|
||||
items: items
|
||||
};
|
||||
};
|
||||
</script>
|
||||
@@ -1,175 +0,0 @@
|
||||
<!-- Blog Preview Tab -->
|
||||
<div class="tab-pane fade" id="blogpreview" role="tabpanel">
|
||||
<div class="row g-4">
|
||||
<!-- Basic Info -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>Basic Information & Blog Selection
|
||||
</h6>
|
||||
<span class="badge bg-info text-dark">CMS will automatically fetch the 3 latest posts if no specific blog is
|
||||
selected.</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input type="text" class="form-control" id="blogPreviewHeading"
|
||||
value="<%= data.blogPreview?.heading || '' %>" placeholder="e.g., Latest Insights & Updates" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subheading</label>
|
||||
<input type="text" class="form-control" id="blogPreviewSubheading"
|
||||
value="<%= data.blogPreview?.subheading || '' %>" placeholder="e.g., Visa Tips & Guides" />
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 mt-4">
|
||||
<label class="form-label fw-bold"><i class="fas fa-check-square me-2"></i>Select Featured Blogs (Direct
|
||||
from Blog Module)</label>
|
||||
<p class="text-muted small mb-3">Select blog posts to prioritize on the home page. If none are selected,
|
||||
the system will use the 3 latest posts.</p>
|
||||
<div class="row g-3 blog-selector-container"
|
||||
style="max-height: 400px; overflow-y: auto; border: 1px solid #eee; padding: 15px; border-radius: 8px;">
|
||||
<% if (allBlogs && allBlogs.length> 0) { %>
|
||||
<% allBlogs.forEach(blog=> {
|
||||
const isSelected = data.blogPreview?.selectedBlogIds && data.blogPreview.selectedBlogIds.some(id =>
|
||||
id.toString() === blog._id.toString());
|
||||
%>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 blog-select-card <%= isSelected ? 'border-primary bg-light' : '' %>"
|
||||
onclick="toggleBlogSelection(this, '<%= blog._id %>')"
|
||||
style="cursor: pointer; transition: all 0.2s;">
|
||||
<div class="position-absolute top-0 end-0 m-2">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input blog-checkbox" type="checkbox" value="<%= blog._id %>"
|
||||
<%=isSelected ? 'checked' : '' %> onclick="event.stopPropagation();
|
||||
handleCheckboxChange(this)">
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
src="<%= blog.featuredImage ? getFullImageUrl(blog.featuredImage, backendUrl) : '/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 %>
|
||||
</h6>
|
||||
<p class="card-text tiny text-muted mb-0">
|
||||
<%= blog.publishedAt ? new Date(blog.publishedAt).toLocaleDateString('vi-VN') : '' %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } else { %>
|
||||
<div class="col-12 text-center py-4">
|
||||
<p class="text-muted">No published blogs found. Please create some blogs first.</p>
|
||||
<a href="/admin/blog/create" class="btn btn-sm btn-outline-primary">Create Blog</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA Button -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-mouse-pointer me-2"></i>CTA Button
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Label</label>
|
||||
<input type="text" class="form-control" id="blogPreviewCtaLabel"
|
||||
value="<%= data.blogPreview?.ctaButton?.label || '' %>" placeholder="e.g., View All Articles" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input type="text" class="form-control" id="blogPreviewCtaHref"
|
||||
value="<%= data.blogPreview?.ctaButton?.href || '' %>" placeholder="/blog" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleBlogSelection(card, blogId) {
|
||||
const checkbox = card.querySelector('.blog-checkbox');
|
||||
const isChecking = !checkbox.checked;
|
||||
|
||||
if (isChecking) {
|
||||
const checkedCount = document.querySelectorAll('.blog-checkbox:checked').length;
|
||||
if (checkedCount >= 3) {
|
||||
alert('You can only select up to 3 blogs.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
checkbox.checked = isChecking;
|
||||
handleCheckboxUpdate(card, checkbox.checked);
|
||||
}
|
||||
|
||||
function handleCheckboxChange(checkbox) {
|
||||
if (checkbox.checked) {
|
||||
const checkedCount = document.querySelectorAll('.blog-checkbox:checked').length;
|
||||
if (checkedCount > 3) {
|
||||
checkbox.checked = false;
|
||||
alert('You can only select up to 3 blogs.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
const card = checkbox.closest('.blog-select-card');
|
||||
handleCheckboxUpdate(card, checkbox.checked);
|
||||
}
|
||||
|
||||
function handleCheckboxUpdate(card, isChecked) {
|
||||
if (isChecked) {
|
||||
card.classList.add('border-primary', 'bg-light');
|
||||
} else {
|
||||
card.classList.remove('border-primary', 'bg-light');
|
||||
}
|
||||
}
|
||||
|
||||
// Đăng ký scraper cho Blog Preview
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
window.homeScrapers.blogPreview = () => {
|
||||
const selectedIds = [];
|
||||
document.querySelectorAll('.blog-checkbox:checked').forEach(cb => {
|
||||
selectedIds.push(cb.value);
|
||||
});
|
||||
|
||||
// Chúng ta vẫn giữ cấu trúc cũ cho items nếu muốn preview offline,
|
||||
// nhưng server sẽ ưu tiên dùng selectedBlogIds để populate dữ liệu mới nhất.
|
||||
return {
|
||||
heading: document.getElementById('blogPreviewHeading').value,
|
||||
subheading: document.getElementById('blogPreviewSubheading').value,
|
||||
ctaButton: {
|
||||
label: document.getElementById('blogPreviewCtaLabel').value,
|
||||
href: document.getElementById('blogPreviewCtaHref').value
|
||||
},
|
||||
selectedBlogIds: selectedIds,
|
||||
items: [] // Server side will handle full items content
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tiny {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.blog-select-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
@@ -1,148 +0,0 @@
|
||||
<!-- FAQ Tab -->
|
||||
<div class="tab-pane fade" id="faq" role="tabpanel">
|
||||
<div class="row g-4">
|
||||
<!-- Basic Info -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input type="text" class="form-control" id="faqHeading" value="<%= data.faq?.heading || '' %>"
|
||||
placeholder="e.g., Got Questions? We've Got Answers" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subheading</label>
|
||||
<input type="text" class="form-control" id="faqSubheading" value="<%= data.faq?.subheading || '' %>"
|
||||
placeholder="e.g., Visa FAQs" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" id="faqDescription" rows="3"
|
||||
placeholder="Enter description"><%= data.faq?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAQ Items -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-question-circle me-2"></i>FAQ Items
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body" id="faqItemsContainer">
|
||||
<%
|
||||
const faqItems = (data.faq && Array.isArray(data.faq.items) && data.faq.items.length === 5)
|
||||
? data.faq.items
|
||||
: (data.faq && Array.isArray(data.faq.items) && data.faq.items.length > 0)
|
||||
? (function () {
|
||||
const clone = data.faq.items.slice(0, 5);
|
||||
while (clone.length < 5) clone.push({});
|
||||
return clone;
|
||||
})()
|
||||
: [{}, {}, {}, {}, {}];
|
||||
%>
|
||||
<% faqItems.forEach(function(item, index) { %>
|
||||
<div class="card mb-3 bg-light border faq-item" data-index="<%= index %>">
|
||||
<div class="card-header d-flex justify-content-between align-items-center bg-white">
|
||||
<h6 class="card-title fw-bold mb-0">FAQ
|
||||
<span class="faq-item-label">
|
||||
<%= index + 1 %>
|
||||
</span>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Question</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="faqQuestion_<%= index %>"
|
||||
value="<%= item.question || '' %>"
|
||||
placeholder="Enter question"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Answer</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="faqAnswer_<%= index %>"
|
||||
rows="3"
|
||||
placeholder="Enter answer"
|
||||
><%= item.answer || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA Button -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-mouse-pointer me-2"></i>CTA Button
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Label</label>
|
||||
<input type="text" class="form-control" id="faqCtaLabel" value="<%= data.faq?.ctaButton?.label || '' %>"
|
||||
placeholder="e.g., contact us" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input type="text" class="form-control" id="faqCtaHref" value="<%= data.faq?.ctaButton?.href || '' %>"
|
||||
placeholder="/contact" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Register scraper to collect FAQ data before form submit
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
window.homeScrapers.faq = () => {
|
||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||
|
||||
const items = [];
|
||||
document.querySelectorAll(".faq-item").forEach((el, idx) => {
|
||||
const index = el.getAttribute("data-index") || idx;
|
||||
|
||||
const question = getVal(`faqQuestion_${index}`);
|
||||
const answer = getVal(`faqAnswer_${index}`);
|
||||
|
||||
if (question || answer) {
|
||||
items.push({ question, answer });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
heading: getVal("faqHeading"),
|
||||
subheading: getVal("faqSubheading"),
|
||||
description: getVal("faqDescription"),
|
||||
ctaButton: {
|
||||
label: getVal("faqCtaLabel"),
|
||||
href: getVal("faqCtaHref")
|
||||
},
|
||||
items
|
||||
};
|
||||
};
|
||||
</script>
|
||||
@@ -1,312 +0,0 @@
|
||||
<!-- Hero Tab -->
|
||||
<div class="tab-pane fade show active" id="hero" role="tabpanel">
|
||||
<div class="row g-4">
|
||||
<!-- Background Image (section-level) -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm mb-3">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-image me-2"></i>Hero Background
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Background Image</label>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="heroBackgroundImage"
|
||||
value="<%= data.hero?.backgroundImage || '' %>" placeholder="/assets/img/home-1/hero/bg.jpg" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="heroBackgroundImage" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<% if (data.hero?.backgroundImage) { %>
|
||||
<div class="mt-2">
|
||||
<img src="<%= data.hero.backgroundImage %>" class="img-thumbnail"
|
||||
style="height: 200px; width: 100%; object-fit: cover;" alt="Background preview" />
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slides -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-sliders-h me-2"></i>Hero Slides
|
||||
</h6>
|
||||
<button type="button" class="btn btn-sm btn-primary" id="addHeroSlideBtn">
|
||||
<i class="fas fa-plus me-1"></i>Add Slide
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body" id="heroSlidesContainer">
|
||||
<% const existingSlides=(data.hero && Array.isArray(data.hero.slides) && data.hero.slides.length> 0)
|
||||
? data.hero.slides
|
||||
: [{
|
||||
title: data.hero?.title || '',
|
||||
subtitle: data.hero?.subtitle || '',
|
||||
description: data.hero?.description || '',
|
||||
heroImage: data.hero?.heroImage || '',
|
||||
videoUrl: data.hero?.videoUrl || '',
|
||||
primaryButton: data.hero?.primaryButton || {},
|
||||
secondaryButton: data.hero?.secondaryButton || {},
|
||||
}];
|
||||
%>
|
||||
|
||||
<% existingSlides.forEach(function(slide, index) { %>
|
||||
<div class="card mb-3 border hero-slide-item" data-index="<%= index %>">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Slide
|
||||
<span class="hero-slide-label">
|
||||
<%= index + 1 %>
|
||||
</span>
|
||||
</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger btn-remove-hero-slide">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_title"
|
||||
value="<%= slide.title || '' %>" placeholder="e.g., From Application to Visa" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subtitle</label>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_subtitle"
|
||||
value="<%= slide.subtitle || '' %>" placeholder="e.g., Global Education Simplified" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" id="heroSlide_<%= index %>_description" rows="3"
|
||||
placeholder="Enter hero description"><%= slide.description || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Hero Image</label>
|
||||
<small class="text-muted d-block mb-1">Recommended size: 893x848px</small>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_heroImage"
|
||||
value="<%= slide.heroImage || '' %>" placeholder="/assets/img/home-1/hero/man.png" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="heroSlide_<%= index %>_heroImage" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<% if (slide.heroImage) { %>
|
||||
<div class="mt-2">
|
||||
<img src="<%= slide.heroImage %>" class="img-thumbnail"
|
||||
style="height: 200px; width: 100%; object-fit: cover;" alt="Hero image preview" />
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Video URL</label>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_videoUrl"
|
||||
value="<%= slide.videoUrl || '' %>" placeholder="https://www.youtube.com/watch?v=..." />
|
||||
</div>
|
||||
|
||||
<!-- Primary Button -->
|
||||
<div class="col-md-6">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<h6 class="fw-semibold mb-3">
|
||||
<i class="fas fa-mouse-pointer me-2"></i>Primary Button
|
||||
</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Label</label>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_primaryLabel"
|
||||
value="<%= slide.primaryButton?.label || '' %>" placeholder="e.g., Apply now" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_primaryHref"
|
||||
value="<%= slide.primaryButton?.href || '' %>" placeholder="/contact" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Secondary Button -->
|
||||
<div class="col-md-6">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<h6 class="fw-semibold mb-3">
|
||||
<i class="fas fa-mouse-pointer me-2"></i>Secondary Button
|
||||
</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Label</label>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_secondaryLabel"
|
||||
value="<%= slide.secondaryButton?.label || '' %>"
|
||||
placeholder="e.g., Book Free Consultation" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_secondaryHref"
|
||||
value="<%= slide.secondaryButton?.href || '' %>" placeholder="/contact" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Register scraper to collect Hero data before form submit
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
window.homeScrapers.hero = () => {
|
||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||
|
||||
const backgroundImage = getVal("heroBackgroundImage");
|
||||
|
||||
const slides = [];
|
||||
const slideEls = document.querySelectorAll(".hero-slide-item");
|
||||
slideEls.forEach((el, idx) => {
|
||||
const index = el.getAttribute("data-index") || idx;
|
||||
const prefix = `heroSlide_${index}_`;
|
||||
|
||||
const slide = {
|
||||
title: getVal(prefix + "title"),
|
||||
subtitle: getVal(prefix + "subtitle"),
|
||||
description: getVal(prefix + "description"),
|
||||
heroImage: getVal(prefix + "heroImage"),
|
||||
videoUrl: getVal(prefix + "videoUrl"),
|
||||
primaryButton: {
|
||||
label: getVal(prefix + "primaryLabel"),
|
||||
href: getVal(prefix + "primaryHref")
|
||||
},
|
||||
secondaryButton: {
|
||||
label: getVal(prefix + "secondaryLabel"),
|
||||
href: getVal(prefix + "secondaryHref")
|
||||
}
|
||||
};
|
||||
|
||||
// Bỏ qua slide trống hoàn toàn
|
||||
const hasContent = slide.title || slide.subtitle || slide.description || slide.heroImage || slide.videoUrl || slide.primaryButton.label || slide.secondaryButton.label;
|
||||
|
||||
if (hasContent) {
|
||||
slides.push(slide);
|
||||
}
|
||||
});
|
||||
|
||||
// Legacy single-slide fields: map từ slide đầu tiên (nếu có) để không vỡ FE cũ
|
||||
const first = slides[0] || {};
|
||||
|
||||
return {
|
||||
backgroundImage,
|
||||
slides,
|
||||
title: first.title || "",
|
||||
subtitle: first.subtitle || "",
|
||||
description: first.description || "",
|
||||
heroImage: first.heroImage || "",
|
||||
videoUrl: first.videoUrl || "",
|
||||
primaryButton: first.primaryButton || {
|
||||
label: "",
|
||||
href: ""
|
||||
},
|
||||
secondaryButton: first.secondaryButton || {
|
||||
label: "",
|
||||
href: ""
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Add / remove slides on the client
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const container = document.getElementById("heroSlidesContainer");
|
||||
const addBtn = document.getElementById("addHeroSlideBtn");
|
||||
|
||||
if (!container || !addBtn)
|
||||
return;
|
||||
|
||||
|
||||
// Re-index slides, update labels, IDs and upload target attributes
|
||||
const updateLabels = () => {
|
||||
container.querySelectorAll(".hero-slide-item").forEach((el, idx) => {
|
||||
el.setAttribute("data-index", String(idx));
|
||||
|
||||
const label = el.querySelector(".hero-slide-label");
|
||||
if (label)
|
||||
label.textContent = String(idx + 1);
|
||||
|
||||
|
||||
// Update input/textarea IDs to match new index
|
||||
el.querySelectorAll("input, textarea").forEach((input) => {
|
||||
if (!input.id)
|
||||
return;
|
||||
|
||||
const newId = input.id.replace(/heroSlide_\d+_/, `heroSlide_${idx}_`);
|
||||
input.id = newId;
|
||||
});
|
||||
|
||||
// Update upload button target-input so upload goes to correct slide
|
||||
el.querySelectorAll(".btn-upload-image").forEach((btn) => {
|
||||
const target = btn.getAttribute("data-target-input") || "";
|
||||
if (!target)
|
||||
return;
|
||||
|
||||
const newTarget = target.replace(/heroSlide_\d+_/, `heroSlide_${idx}_`);
|
||||
btn.setAttribute("data-target-input", newTarget);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
addBtn.addEventListener("click", () => {
|
||||
const template = container.querySelector(".hero-slide-item");
|
||||
if (!template)
|
||||
return;
|
||||
|
||||
|
||||
const clone = template.cloneNode(true);
|
||||
|
||||
// Clear values for the cloned slide (IDs will be fixed by updateLabels)
|
||||
clone.querySelectorAll("input, textarea").forEach((input) => {
|
||||
input.value = "";
|
||||
});
|
||||
|
||||
// Clear image previews
|
||||
clone.querySelectorAll("img").forEach((img) => {
|
||||
img.classList.add("d-none");
|
||||
img.removeAttribute("src");
|
||||
});
|
||||
|
||||
container.appendChild(clone);
|
||||
updateLabels();
|
||||
});
|
||||
|
||||
container.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest(".btn-remove-hero-slide");
|
||||
if (!btn)
|
||||
return;
|
||||
|
||||
|
||||
const card = btn.closest(".hero-slide-item");
|
||||
if (!card)
|
||||
return;
|
||||
|
||||
|
||||
const all = container.querySelectorAll(".hero-slide-item");
|
||||
if (all.length <= 1) { // Không cho xóa slide cuối cùng
|
||||
return;
|
||||
}
|
||||
|
||||
card.remove();
|
||||
updateLabels();
|
||||
});
|
||||
|
||||
// Initial normalization (in case indices rendered from server are not 0..n)
|
||||
updateLabels();
|
||||
});
|
||||
</script>
|
||||
@@ -1,150 +0,0 @@
|
||||
<!-- Partners Tab -->
|
||||
<div class="tab-pane fade" id="partners" role="tabpanel">
|
||||
<div class="row g-4">
|
||||
<!-- Visa Consultancy Awards -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-award me-2"></i>Awards & Certifications (Fixed 4 Items)
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="visaConsultancyContainer">
|
||||
<% for(let i=0; i<4; i++) {
|
||||
const item = (data.partners?.visaConsultancy?.items && data.partners.visaConsultancy.items[i]) || {};
|
||||
%>
|
||||
<div class="card mb-3 bg-light border visa-item">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="card-title fw-bold mb-0">Award #<%= i + 1 %></h6>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label fw-medium">Award Name</label>
|
||||
<input type="text" class="form-control visa-name" value="<%= item.name || '' %>" placeholder="Award Name" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-medium">Year</label>
|
||||
<input type="text" class="form-control visa-year" value="<%= item.year || '' %>" placeholder="e.g., 2025" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Icon / Logo</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control visa-icon" id="visaIcon_<%= i %>" value="<%= item.icon || '' %>" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="visaIcon_<%= i %>" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2 preview-container">
|
||||
<img src="<%= item.icon || '' %>" class="img-thumbnail <%= item.icon ? '' : 'd-none' %>" style="height: 60px; object-fit: contain;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brand Logos -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-images me-2"></i>Brand Partner Logos (Slider)
|
||||
</h6>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="addBrandPartnerItem()">
|
||||
<i class="fas fa-plus me-1"></i>Add Logo
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="brandPartnersContainer" class="row g-3">
|
||||
<% (data.partners?.brands?.items || []).forEach(function(item, index) { %>
|
||||
<div class="col-md-4 brand-partner-item">
|
||||
<div class="card border bg-light h-100">
|
||||
<div class="card-body py-2">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="small fw-bold">Brand Logo</span>
|
||||
<button type="button" class="btn btn-link text-danger p-0" onclick="this.closest('.brand-partner-item').remove()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control brand-logo-input" id="brandLogo_<%= index %>" value="<%= item.logo || '' %>" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="brandLogo_<%= index %>" data-image-type="home">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Thu thập dữ liệu partners
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
window.homeScrapers.partners = function() {
|
||||
const visaItems = [];
|
||||
document.querySelectorAll('.visa-item').forEach(el => {
|
||||
visaItems.push({
|
||||
name: el.querySelector('.visa-name').value,
|
||||
year: el.querySelector('.visa-year').value,
|
||||
icon: el.querySelector('.visa-icon').value
|
||||
});
|
||||
});
|
||||
|
||||
const brandItems = [];
|
||||
document.querySelectorAll('.brand-partner-item').forEach(el => {
|
||||
const logo = el.querySelector('.brand-logo-input').value;
|
||||
if (logo) brandItems.push({ logo: logo });
|
||||
});
|
||||
|
||||
return {
|
||||
visaConsultancy: { items: visaItems },
|
||||
brands: { items: brandItems }
|
||||
};
|
||||
};
|
||||
|
||||
function addBrandPartnerItem() {
|
||||
const container = document.getElementById('brandPartnersContainer');
|
||||
const id = 'brandLogo_' + Date.now();
|
||||
const div = document.createElement('div');
|
||||
div.className = 'col-md-4 brand-partner-item';
|
||||
div.innerHTML = `
|
||||
<div class="card border bg-light h-100">
|
||||
<div class="card-body py-2">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="small fw-bold">New Brand Logo</span>
|
||||
<button type="button" class="btn btn-link text-danger p-0" onclick="this.closest('.brand-partner-item').remove()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control brand-logo-input" id="${id}">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="${id}" data-image-type="home">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2 text-center preview-container">
|
||||
<img src="" class="img-thumbnail d-none" style="height: 50px; object-fit: contain;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
}
|
||||
</script>
|
||||
@@ -1,211 +0,0 @@
|
||||
<!-- Testimonials Tab -->
|
||||
<div class="tab-pane fade" id="testimonials" role="tabpanel">
|
||||
<div class="row g-4">
|
||||
<!-- Basic Info -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input type="text" class="form-control" id="testimonialsHeading"
|
||||
value="<%= data.testimonials?.heading || '' %>" placeholder="e.g., Student Reviews & Testimonials" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subheading</label>
|
||||
<input type="text" class="form-control" id="testimonialsSubheading"
|
||||
value="<%= data.testimonials?.subheading || '' %>" placeholder="e.g., What Our Students Say" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Video URL</label>
|
||||
<input type="text" class="form-control" id="testimonialsVideoUrl"
|
||||
value="<%= data.testimonials?.videoUrl || '' %>" placeholder="https://www.youtube.com/watch?v=..." />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Video Thumbnail</label>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="testimonialsVideoThumbnail"
|
||||
value="<%= data.testimonials?.videoThumbnail || '' %>" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="testimonialsVideoThumbnail" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Testimonial Items -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-comments me-2"></i>Testimonials
|
||||
</h6>
|
||||
<button type="button" class="btn btn-sm btn-primary" id="addTestimonialBtn">
|
||||
<i class="fas fa-plus me-1"></i>Add Testimonial
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body" id="testimonialsItemsContainer">
|
||||
<% (data.testimonials?.items || []).forEach(function(item, index) { %>
|
||||
<div class="card mb-3 bg-light border testimonial-item" data-index="<%= index %>">
|
||||
<div class="card-header d-flex justify-content-between align-items-center bg-white">
|
||||
<h6 class="card-title fw-bold mb-0">Testimonial <span class="testimonial-label">
|
||||
<%= index + 1 %>
|
||||
</span></h6>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger btn-remove-testimonial">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Name</label>
|
||||
<input type="text" class="form-control" id="testimonialsName_<%= index %>"
|
||||
value="<%= item.name || '' %>" placeholder="e.g., Sohel Tanvir" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Role</label>
|
||||
<input type="text" class="form-control" id="testimonialsRole_<%= index %>"
|
||||
value="<%= item.role || '' %>" placeholder="e.g., Student" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Country</label>
|
||||
<input type="text" class="form-control" id="testimonialsCountry_<%= index %>"
|
||||
value="<%= item.country || '' %>" placeholder="e.g., Canada" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Rating</label>
|
||||
<input type="number" class="form-control" id="testimonialsRating_<%= index %>"
|
||||
value="<%= item.rating || 5 %>" min="1" max="5" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Comment</label>
|
||||
<textarea class="form-control" id="testimonialsComment_<%= index %>" rows="3"
|
||||
placeholder="Enter testimonial comment"><%= item.comment || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Avatar</label>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="testimonialsAvatar_<%= index %>"
|
||||
value="<%= item.avatar || '' %>" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="testimonialsAvatar_<%= index %>" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Register scraper to collect Testimonials data before form submit
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
window.homeScrapers.testimonials = () => {
|
||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||
|
||||
const items = [];
|
||||
document.querySelectorAll(".testimonial-item").forEach((el, idx) => {
|
||||
const index = el.getAttribute("data-index") || idx;
|
||||
|
||||
const name = getVal(`testimonialsName_${index}`);
|
||||
const role = getVal(`testimonialsRole_${index}`);
|
||||
const country = getVal(`testimonialsCountry_${index}`);
|
||||
const ratingStr = getVal(`testimonialsRating_${index}`);
|
||||
const rating = ratingStr ? parseInt(ratingStr, 10) : 5;
|
||||
const comment = getVal(`testimonialsComment_${index}`);
|
||||
const avatar = getVal(`testimonialsAvatar_${index}`);
|
||||
|
||||
if (name || role || country || comment || avatar) {
|
||||
items.push({
|
||||
name,
|
||||
role,
|
||||
country,
|
||||
rating: isNaN(rating) ? 5 : Math.min(Math.max(rating, 1), 5),
|
||||
comment,
|
||||
avatar,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
heading: getVal("testimonialsHeading"),
|
||||
subheading: getVal("testimonialsSubheading"),
|
||||
videoUrl: getVal("testimonialsVideoUrl"),
|
||||
videoThumbnail: getVal("testimonialsVideoThumbnail"),
|
||||
items,
|
||||
};
|
||||
};
|
||||
|
||||
// Client-side add/remove for testimonials
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const container = document.getElementById("testimonialsItemsContainer");
|
||||
const addBtn = document.getElementById("addTestimonialBtn");
|
||||
|
||||
if (!container || !addBtn) return;
|
||||
|
||||
const updateLabels = () => {
|
||||
container.querySelectorAll(".testimonial-item").forEach((el, idx) => {
|
||||
el.setAttribute("data-index", String(idx));
|
||||
|
||||
const label = el.querySelector(".testimonial-label");
|
||||
if (label) label.textContent = String(idx + 1);
|
||||
|
||||
el.querySelectorAll("input, textarea").forEach((input) => {
|
||||
if (!input.id) return;
|
||||
const newId = input.id.replace(/testimonials\w+_\d+/, (match) => {
|
||||
const [prefix] = match.split("_");
|
||||
return `${prefix}_${idx}`;
|
||||
});
|
||||
input.id = newId;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
addBtn.addEventListener("click", () => {
|
||||
const template = container.querySelector(".testimonial-item");
|
||||
if (!template) return;
|
||||
|
||||
const clone = template.cloneNode(true);
|
||||
|
||||
clone.querySelectorAll("input, textarea").forEach((input) => {
|
||||
input.value = "";
|
||||
});
|
||||
|
||||
container.appendChild(clone);
|
||||
updateLabels();
|
||||
});
|
||||
|
||||
container.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest(".btn-remove-testimonial");
|
||||
if (!btn) return;
|
||||
|
||||
const card = btn.closest(".testimonial-item");
|
||||
if (!card) return;
|
||||
|
||||
const all = container.querySelectorAll(".testimonial-item");
|
||||
if (all.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
card.remove();
|
||||
updateLabels();
|
||||
});
|
||||
|
||||
updateLabels();
|
||||
});
|
||||
</script>
|
||||
@@ -1,59 +0,0 @@
|
||||
<!-- Video Gallery Tab -->
|
||||
<div class="tab-pane fade" id="videogallery" role="tabpanel">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-video me-2"></i>Video Gallery
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input type="text" class="form-control" id="videoGalleryHeading"
|
||||
value="<%= data.videoGallery?.heading || '' %>" placeholder="e.g., VIDEO PLAY GALLERY" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Video URL</label>
|
||||
<input type="text" class="form-control" id="videoGalleryVideoUrl"
|
||||
value="<%= data.videoGallery?.videoUrl || '' %>" placeholder="https://example.com/video.mp4" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Thumbnail Image</label>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="videoGalleryThumbnail"
|
||||
value="<%= data.videoGallery?.thumbnail || '' %>" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="videoGalleryThumbnail" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<% if (data.videoGallery?.thumbnail) { %>
|
||||
<div class="mt-2">
|
||||
<img src="<%= data.videoGallery.thumbnail %>" class="img-thumbnail"
|
||||
style="height: 200px; width: 100%; object-fit: cover;" alt="Thumbnail preview" />
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Register scraper to collect Video Gallery data before form submit
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
window.homeScrapers.videoGallery = () => {
|
||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||
|
||||
return {
|
||||
heading: getVal("videoGalleryHeading"),
|
||||
videoUrl: getVal("videoGalleryVideoUrl"),
|
||||
thumbnail: getVal("videoGalleryThumbnail"),
|
||||
};
|
||||
};
|
||||
</script>
|
||||
@@ -1,145 +0,0 @@
|
||||
<!-- Visa Countries Tab -->
|
||||
<div class="tab-pane fade" id="visacountries" role="tabpanel">
|
||||
<div class="row g-4">
|
||||
<!-- Basic Info -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input type="text" class="form-control" id="visaCountriesHeading"
|
||||
value="<%= data.visaCountries?.heading || '' %>" placeholder="e.g., Visa & VISAWAY Services To UK" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subheading</label>
|
||||
<input type="text" class="form-control" id="visaCountriesSubheading"
|
||||
value="<%= data.visaCountries?.subheading || '' %>" placeholder="e.g., UK. United Kingdom" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" id="visaCountriesDescription" rows="3"
|
||||
placeholder="Enter description"><%= data.visaCountries?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Countries (Featured Country configuration) -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-globe me-2"></i>Featured Country
|
||||
</h6>
|
||||
<small class="text-muted">This country is used in the home page feature section.</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% const featured=(data.visaCountries && Array.isArray(data.visaCountries.countries) &&
|
||||
data.visaCountries.countries.length> 0)
|
||||
? data.visaCountries.countries[0]
|
||||
: {};
|
||||
%>
|
||||
<div class="card mb-3 bg-light border visa-country-item" data-index="0">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Country Name</label>
|
||||
<input type="text" class="form-control" id="visaCountriesName_0" value="<%= featured.name || '' %>"
|
||||
placeholder="e.g., United Kingdom" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Country Code</label>
|
||||
<input type="text" class="form-control" id="visaCountriesCode_0" value="<%= featured.code || '' %>"
|
||||
placeholder="e.g., UK" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Flag / Illustration Image</label>
|
||||
<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" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="visaCountriesFlag_0" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input type="text" class="form-control" id="visaCountriesLink_0" value="<%= featured.link || '' %>"
|
||||
placeholder="/country-details/uk" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Visa Types (comma-separated)</label>
|
||||
<textarea class="form-control" id="visaCountriesVisaTypes_0" rows="2"
|
||||
placeholder="e.g., Student Visa, Work Visa, Tourist Visa"><%= (featured.visaTypes || []).join(', ') %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA Button -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-mouse-pointer me-2"></i>CTA Button
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Label</label>
|
||||
<input type="text" class="form-control" id="visaCountriesCtaLabel"
|
||||
value="<%= data.visaCountries?.ctaButton?.label || '' %>" placeholder="e.g., Get Started" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input type="text" class="form-control" id="visaCountriesCtaHref"
|
||||
value="<%= data.visaCountries?.ctaButton?.href || '' %>" placeholder="/contact" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Register scraper to collect Visa Countries data before form submit
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
window.homeScrapers.visaCountries = () => {
|
||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||
|
||||
const visaTypesRaw = getVal("visaCountriesVisaTypes_0");
|
||||
const visaTypes = visaTypesRaw ? visaTypesRaw.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
||||
|
||||
const featuredCountry = {
|
||||
name: getVal("visaCountriesName_0"),
|
||||
code: getVal("visaCountriesCode_0"),
|
||||
flag: getVal("visaCountriesFlag_0"),
|
||||
link: getVal("visaCountriesLink_0"),
|
||||
visaTypes
|
||||
};
|
||||
|
||||
return {
|
||||
heading: getVal("visaCountriesHeading"),
|
||||
subheading: getVal("visaCountriesSubheading"),
|
||||
description: getVal("visaCountriesDescription"),
|
||||
countries: [featuredCountry],
|
||||
ctaButton: {
|
||||
label: getVal("visaCountriesCtaLabel"),
|
||||
href: getVal("visaCountriesCtaHref")
|
||||
}
|
||||
};
|
||||
};
|
||||
</script>
|
||||
@@ -1,137 +0,0 @@
|
||||
<!-- Visa Solutions Tab -->
|
||||
<div class="tab-pane fade" id="visasolutions" role="tabpanel">
|
||||
<div class="row g-4">
|
||||
<!-- Basic Info -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input type="text" class="form-control" id="visaSolutionsHeading"
|
||||
value="<%= data.visaSolutions?.heading || '' %>" placeholder="e.g., Comprehensive Visa Solutions" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subheading</label>
|
||||
<input type="text" class="form-control" id="visaSolutionsSubheading"
|
||||
value="<%= data.visaSolutions?.subheading || '' %>" placeholder="e.g., Our Expert Services" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Services Items -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-list-ul me-2"></i>Visa Solutions Items
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body" id="visaSolutionsItemsContainer">
|
||||
<% const vsItems=(data.visaSolutions && Array.isArray(data.visaSolutions.items) &&
|
||||
data.visaSolutions.items.length===4) ? data.visaSolutions.items : (data.visaSolutions &&
|
||||
Array.isArray(data.visaSolutions.items) && data.visaSolutions.items.length> 0)
|
||||
? (function () {
|
||||
const clone = data.visaSolutions.items.slice(0, 4);
|
||||
while (clone.length < 4) clone.push({}); return clone; })() : [{}, {}, {}, {}]; %>
|
||||
<% vsItems.forEach(function(item, index) { %>
|
||||
<div class="card mb-3 bg-light border visa-solution-item" data-index="<%= index %>">
|
||||
<div class="card-header d-flex justify-content-between align-items-center bg-white">
|
||||
<h6 class="card-title fw-bold mb-0">Service <span class="visa-solution-label">
|
||||
<%= index + 1 %>
|
||||
</span></h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-medium">Number</label>
|
||||
<input type="text" class="form-control" id="visaSolutionsNumber_<%= index %>"
|
||||
value="<%= item.number || '' %>" placeholder="e.g., 01" />
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="visaSolutionsTitle_<%= index %>"
|
||||
value="<%= item.title || '' %>" placeholder="e.g., Student Visa Guidance" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" id="visaSolutionsDescription_<%= index %>" rows="2"
|
||||
placeholder="Enter description"><%= item.description || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input type="text" class="form-control" id="visaSolutionsLink_<%= index %>"
|
||||
value="<%= item.link || '' %>" placeholder="/service-details" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Register scraper to collect Visa Solutions data before form submit
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
window.homeScrapers.visaSolutions = () => {
|
||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||
|
||||
const items = [];
|
||||
document.querySelectorAll(".visa-solution-item").forEach((el, idx) => {
|
||||
const index = el.getAttribute("data-index") || idx;
|
||||
|
||||
const number = getVal(`visaSolutionsNumber_${index}`);
|
||||
const title = getVal(`visaSolutionsTitle_${index}`);
|
||||
const description = getVal(`visaSolutionsDescription_${index}`);
|
||||
const link = getVal(`visaSolutionsLink_${index}`);
|
||||
|
||||
if (number || title || description || link) {
|
||||
items.push({ number, title, description, link });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
heading: getVal("visaSolutionsHeading"),
|
||||
subheading: getVal("visaSolutionsSubheading"),
|
||||
items,
|
||||
};
|
||||
};
|
||||
|
||||
// Client-side add/remove for visa solutions
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const container = document.getElementById("visaSolutionsItemsContainer");
|
||||
if (!container) return;
|
||||
|
||||
const updateLabels = () => {
|
||||
const items = container.querySelectorAll(".visa-solution-item");
|
||||
|
||||
items.forEach((el, idx) => {
|
||||
el.setAttribute("data-index", String(idx));
|
||||
|
||||
const label = el.querySelector(".visa-solution-label");
|
||||
if (label) label.textContent = String(idx + 1);
|
||||
|
||||
el.querySelectorAll("input, textarea").forEach((input) => {
|
||||
if (!input.id) return;
|
||||
const newId = input.id.replace(/visaSolutions\w+_\d+/, (match) => {
|
||||
const [prefix] = match.split("_");
|
||||
return `${prefix}_${idx}`;
|
||||
});
|
||||
input.id = newId;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
updateLabels();
|
||||
});
|
||||
</script>
|
||||
@@ -1,233 +0,0 @@
|
||||
<!-- Why Choose Us Tab -->
|
||||
<div class="tab-pane fade" id="whychooseus" role="tabpanel">
|
||||
<div class="row g-4">
|
||||
<!-- Basic Info -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm mb-3">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input type="text" class="form-control" id="whyChooseUsHeading"
|
||||
value="<%= data.whyChooseUs?.heading || '' %>"
|
||||
placeholder="e.g., Turning Study Abroad Dreams Into Reality" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subheading</label>
|
||||
<input type="text" class="form-control" id="whyChooseUsSubheading"
|
||||
value="<%= data.whyChooseUs?.subheading || '' %>" placeholder="e.g., About Our Consultancy" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Highlight Word (Optional)</label>
|
||||
<input type="text" class="form-control" id="whyChooseUsHighlightWord"
|
||||
value="<%= data.whyChooseUs?.highlightWord || '' %>" placeholder="e.g., Dreams" />
|
||||
<small class="text-muted">This word in the heading will be wrapped in a colored span.</small>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" id="whyChooseUsDescription" rows="3"
|
||||
placeholder="Enter description"><%= data.whyChooseUs?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- About Images -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm mb-3">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-images me-2"></i>About Images (Left side)
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Main Image</label>
|
||||
<small class="text-muted d-block mb-1">Recommended size: 375x419px</small>
|
||||
<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" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="whyChooseUsMainImage" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<% if (data.whyChooseUs?.mainImage) { %>
|
||||
<div class="mt-2">
|
||||
<img src="<%= data.whyChooseUs.mainImage %>" class="img-thumbnail"
|
||||
style="height: 200px; width: 100%; object-fit: cover;" alt="Main about image preview" />
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Secondary Image</label>
|
||||
<small class="text-muted d-block mb-1">Recommended size: 376x394px</small>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="whyChooseUsSecondaryImage"
|
||||
value="<%= data.whyChooseUs?.secondaryImage || '' %>"
|
||||
placeholder="/assets/img/home-1/about/about-02.jpg" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="whyChooseUsSecondaryImage" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<% if (data.whyChooseUs?.secondaryImage) { %>
|
||||
<div class="mt-2">
|
||||
<img src="<%= data.whyChooseUs.secondaryImage %>" class="img-thumbnail"
|
||||
style="height: 200px; width: 100%; object-fit: cover;" alt="Secondary about image preview" />
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Items -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-list-ul me-2"></i>Why Choose Us Items
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% (data.whyChooseUs?.items || []).forEach(function(item, index) { %>
|
||||
<div class="card mb-3 bg-light border">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title fw-bold mb-3">Item <%= index + 1 %>
|
||||
</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Icon URL</label>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="whyChooseUsIcon_<%= index %>"
|
||||
value="<%= item.icon || '' %>" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="whyChooseUsIcon_<%= index %>" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="whyChooseUsTitle_<%= index %>"
|
||||
value="<%= item.title || '' %>" placeholder="e.g., Global Reach" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<input type="text" class="form-control" id="whyChooseUsItemDescription_<%= index %>"
|
||||
value="<%= item.description || '' %>" placeholder="e.g., Expanding Opportunities Worldwide" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-star me-2"></i>Features
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% (data.whyChooseUs?.features || []).forEach(function(feature, index) { %>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">Feature <%= index + 1 %></label>
|
||||
<input type="text" class="form-control" id="whyChooseUsFeature_<%= index %>" value="<%= feature %>"
|
||||
placeholder="Enter feature" />
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA Button -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-mouse-pointer me-2"></i>CTA Button
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Label</label>
|
||||
<input type="text" class="form-control" id="whyChooseUsCtaLabel"
|
||||
value="<%= data.whyChooseUs?.ctaButton?.label || '' %>" placeholder="e.g., Get Started" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input type="text" class="form-control" id="whyChooseUsCtaHref"
|
||||
value="<%= data.whyChooseUs?.ctaButton?.href || '' %>" placeholder="/about" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Register scraper to collect Why Choose Us data before form submit
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
window.homeScrapers.whyChooseUs = () => {
|
||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||
|
||||
// Collect items
|
||||
const items = [];
|
||||
document.querySelectorAll('input[id^="whyChooseUsTitle_"]').forEach((titleInput) => {
|
||||
const id = titleInput.id || "";
|
||||
const match = id.match(/whyChooseUsTitle_(\d+)/);
|
||||
if (!match) return;
|
||||
const idx = match[1];
|
||||
|
||||
const title = (titleInput.value || "").trim();
|
||||
const icon = getVal(`whyChooseUsIcon_${idx}`);
|
||||
const description = getVal(`whyChooseUsItemDescription_${idx}`);
|
||||
|
||||
// Bỏ qua item trống hoàn toàn
|
||||
if (title || icon || description) {
|
||||
items.push({ icon, title, description });
|
||||
}
|
||||
});
|
||||
|
||||
// Collect features
|
||||
const features = [];
|
||||
document.querySelectorAll('input[id^="whyChooseUsFeature_"]').forEach((featureInput) => {
|
||||
const value = (featureInput.value || "").trim();
|
||||
if (value) {
|
||||
features.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
heading: getVal("whyChooseUsHeading"),
|
||||
subheading: getVal("whyChooseUsSubheading"),
|
||||
description: getVal("whyChooseUsDescription"),
|
||||
highlightWord: getVal("whyChooseUsHighlightWord"),
|
||||
mainImage: getVal("whyChooseUsMainImage"),
|
||||
secondaryImage: getVal("whyChooseUsSecondaryImage"),
|
||||
items,
|
||||
features,
|
||||
ctaButton: {
|
||||
label: getVal("whyChooseUsCtaLabel"),
|
||||
href: getVal("whyChooseUsCtaHref"),
|
||||
},
|
||||
};
|
||||
};
|
||||
</script>
|
||||
@@ -1,438 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
|
||||
<%= title %>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Manage testimonials section</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="<%= frontendUrl %>/" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-2"></i>View Homepage
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form method="POST" class="content-with-fixed-buttons" id="testimonialForm"
|
||||
action=" /update">
|
||||
<!-- Hidden input for items JSON data -->
|
||||
<input type="hidden" name="items" id="itemsJson">
|
||||
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0"><i class="fas fa-quote-left me-2"></i>Testimonials Settings</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- Header Section -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input type="text" class="form-control" name="heading" id="heading"
|
||||
value="<%= data.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subheading</label>
|
||||
<input type="text" class="form-control" name="subheading" id="subheading"
|
||||
value="<%= data.subheading || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Video Section -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Video URL</label>
|
||||
<input type="text" class="form-control" name="videoUrl" id="videoUrl"
|
||||
value="<%= data.videoUrl || '' %>"
|
||||
placeholder="https://www.youtube.com/watch?v=...">
|
||||
<small class="text-muted">YouTube video URL</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Video Thumbnail</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="videoThumbnail" id="videoThumbnail"
|
||||
value="<%= data.videoThumbnail || '' %>">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="videoThumbnail" data-image-type="testimonial">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<% if (data.videoThumbnail) { %>
|
||||
<img src="<%= data.videoThumbnail %>" class="img-thumbnail mt-2"
|
||||
style="max-height: 100px;" alt="Thumbnail preview">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<!-- Testimonial Items -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-medium mb-0">Testimonial Items</h6>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="addTestimonialItem()">
|
||||
<i class="fas fa-plus"></i> Add Testimonial
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="testimonialItemsContainer">
|
||||
<% if (data.items && data.items.length> 0) { %>
|
||||
<% data.items.forEach((item, index)=> { %>
|
||||
<div class="card mb-3 testimonial-item">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" name="itemName_<%= index %>"
|
||||
value="<%= item.name || '' %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Role</label>
|
||||
<input type="text" class="form-control" name="itemRole_<%= index %>"
|
||||
value="<%= item.role || '' %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Country</label>
|
||||
<input type="text" class="form-control"
|
||||
name="itemCountry_<%= index %>"
|
||||
value="<%= item.country || '' %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Rating (1-5)</label>
|
||||
<select class="form-select" name="itemRating_<%= index %>">
|
||||
<% for (let i=1; i <=5; i++) { %>
|
||||
<option value="<%= i %>" <%=item.rating===i ? 'selected'
|
||||
: '' %>>
|
||||
<%= i %> Star<%= i> 1 ? 's' : '' %>
|
||||
</option>
|
||||
<% } %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Comment</label>
|
||||
<textarea class="form-control" name="itemComment_<%= index %>"
|
||||
rows="3"><%= item.comment || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Avatar</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control"
|
||||
name="itemAvatar_<%= index %>"
|
||||
value="<%= item.avatar || '' %>">
|
||||
<button type="button"
|
||||
class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="itemAvatar_<%= index %>"
|
||||
data-image-type="testimonial">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<% if (item.avatar) { %>
|
||||
<img src="<%= item.avatar %>"
|
||||
class="img-thumbnail mt-2 avatar-preview"
|
||||
style="max-height: 60px; max-width: 60px; border-radius: 50%;"
|
||||
alt="Avatar">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm mt-3"
|
||||
onclick="removeTestimonialItem(this)">
|
||||
<i class="fas fa-trash me-2"></i>Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fixed Bottom Buttons -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
|
||||
<i class="fas fa-undo me-2"></i>Reset
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<i class="fas fa-save me-2"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="application/json" id="testimonialDataJson"><%- JSON.stringify(data) %></script>
|
||||
|
||||
<script>
|
||||
let originalFormData = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
try {
|
||||
var jsonScript = document.getElementById('testimonialDataJson');
|
||||
originalFormData = JSON.parse(jsonScript.textContent);
|
||||
} catch (e) {
|
||||
console.error('Error parsing originalFormData:', e);
|
||||
originalFormData = { items: [] };
|
||||
}
|
||||
|
||||
initializeFormHandlers();
|
||||
});
|
||||
|
||||
function initializeFormHandlers() {
|
||||
const form = document.getElementById('testimonialForm');
|
||||
form.addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
|
||||
|
||||
try {
|
||||
updateItemsJson();
|
||||
this.submit();
|
||||
} catch (error) {
|
||||
console.error('Error updating data:', error);
|
||||
alert('Failed to process form data. Please try again.');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
const imageType = this.dataset.imageType;
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateItemsJson() {
|
||||
const items = [];
|
||||
document.querySelectorAll('.testimonial-item').forEach((item, index) => {
|
||||
const name = item.querySelector(`[name="itemName_${index}"]`)?.value ||
|
||||
item.querySelector('[name^="itemName_"]')?.value || '';
|
||||
const role = item.querySelector(`[name="itemRole_${index}"]`)?.value ||
|
||||
item.querySelector('[name^="itemRole_"]')?.value || '';
|
||||
const country = item.querySelector(`[name="itemCountry_${index}"]`)?.value ||
|
||||
item.querySelector('[name^="itemCountry_"]')?.value || '';
|
||||
const rating = parseInt(item.querySelector(`[name="itemRating_${index}"]`)?.value ||
|
||||
item.querySelector('[name^="itemRating_"]')?.value || '5');
|
||||
const comment = item.querySelector(`[name="itemComment_${index}"]`)?.value ||
|
||||
item.querySelector('[name^="itemComment_"]')?.value || '';
|
||||
const avatar = item.querySelector(`[name="itemAvatar_${index}"]`)?.value ||
|
||||
item.querySelector('[name^="itemAvatar_"]')?.value || '';
|
||||
|
||||
items.push({ name, role, country, rating, comment, avatar });
|
||||
});
|
||||
|
||||
document.getElementById('itemsJson').value = JSON.stringify(items);
|
||||
}
|
||||
|
||||
function addTestimonialItem() {
|
||||
const container = document.getElementById('testimonialItemsContainer');
|
||||
const index = container.querySelectorAll('.testimonial-item').length;
|
||||
|
||||
const html = `
|
||||
<div class="card mb-3 testimonial-item">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" name="itemName_${index}" value="">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Role</label>
|
||||
<input type="text" class="form-control" name="itemRole_${index}" value="">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Country</label>
|
||||
<input type="text" class="form-control" name="itemCountry_${index}" value="">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Rating (1-5)</label>
|
||||
<select class="form-select" name="itemRating_${index}">
|
||||
<option value="1">1 Star</option>
|
||||
<option value="2">2 Stars</option>
|
||||
<option value="3">3 Stars</option>
|
||||
<option value="4">4 Stars</option>
|
||||
<option value="5" selected>5 Stars</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Comment</label>
|
||||
<textarea class="form-control" name="itemComment_${index}" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Avatar</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="itemAvatar_${index}" value="">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="itemAvatar_${index}" data-image-type="testimonial">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm mt-3"
|
||||
onclick="removeTestimonialItem(this)">
|
||||
<i class="fas fa-trash me-2"></i>Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
|
||||
// Re-attach upload handlers for new item
|
||||
container.querySelector('.testimonial-item:last-child .btn-upload-image').addEventListener('click', function () {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
const imageType = this.dataset.imageType;
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
}
|
||||
|
||||
function removeTestimonialItem(button) {
|
||||
if (confirm('Are you sure you want to remove this testimonial?')) {
|
||||
button.closest('.testimonial-item').remove();
|
||||
reindexItems();
|
||||
}
|
||||
}
|
||||
|
||||
function reindexItems() {
|
||||
document.querySelectorAll('.testimonial-item').forEach((item, index) => {
|
||||
item.querySelectorAll('[name^="item"]').forEach(input => {
|
||||
const baseName = input.name.replace(/_\d+$/, '');
|
||||
input.name = `${baseName}_${index}`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
if (confirm('Are you sure you want to reset all changes?')) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
function openImageUploader(targetInput, imageType) {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.style.display = 'none';
|
||||
document.body.appendChild(fileInput);
|
||||
|
||||
fileInput.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : 'Upload';
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
|
||||
}
|
||||
|
||||
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Upload failed');
|
||||
}
|
||||
|
||||
const input = document.getElementById(targetInput) || document.querySelector(`[name="${targetInput}"]`);
|
||||
if (!input) {
|
||||
throw new Error('Target input not found');
|
||||
}
|
||||
|
||||
input.value = result.path;
|
||||
|
||||
// Update preview if exists
|
||||
const previewUrl = (result.path && (result.path.startsWith('http://') || result.path.startsWith('https://')))
|
||||
? result.path
|
||||
: (window.location.origin + result.path);
|
||||
|
||||
const parent = input.closest('.col-md-4') || input.closest('div');
|
||||
let previewImg = parent.querySelector('.avatar-preview, .img-thumbnail');
|
||||
|
||||
if (previewImg) {
|
||||
previewImg.src = previewUrl;
|
||||
} else {
|
||||
const img = document.createElement('img');
|
||||
img.src = previewUrl;
|
||||
img.className = 'img-thumbnail mt-2';
|
||||
img.style.maxHeight = '60px';
|
||||
img.alt = 'Preview';
|
||||
input.closest('.input-group').after(img);
|
||||
}
|
||||
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
}
|
||||
|
||||
showToast('Success', 'Image uploaded successfully', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
showToast('Error', 'Failed to upload image: ' + error.message, 'error');
|
||||
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = '<i class="fas fa-upload me-1"></i>Upload';
|
||||
}
|
||||
} finally {
|
||||
document.body.removeChild(fileInput);
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.click();
|
||||
}
|
||||
|
||||
function showToast(title, message, type = 'info') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast align-items-center text-white bg-${type === 'error' ? 'danger' : type} border-0`;
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.setAttribute('aria-live', 'assertive');
|
||||
toast.setAttribute('aria-atomic', 'true');
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<strong>${title}:</strong> ${message}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let container = document.querySelector('.toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.className = 'toast-container position-fixed top-0 end-0 p-3';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
container.appendChild(toast);
|
||||
|
||||
const bsToast = new bootstrap.Toast(toast, {
|
||||
animation: true,
|
||||
autohide: true,
|
||||
delay: 3000
|
||||
});
|
||||
bsToast.show();
|
||||
|
||||
toast.addEventListener('hidden.bs.toast', () => {
|
||||
toast.remove();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1,206 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
|
||||
<%= title %>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Manage video gallery section on homepage</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="<%= frontendUrl %>/" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-2"></i>View Homepage
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form method="POST" class="content-with-fixed-buttons" id="videoGalleryForm"
|
||||
action="/admin/video-gallery/update">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0"><i class="fas fa-video me-2"></i>Video Gallery Settings</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input type="text" class="form-control" name="heading" id="heading"
|
||||
value="<%= data.heading || '' %>" placeholder="e.g., Explore World">
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Video URL</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="videoUrl" id="videoUrl"
|
||||
value="<%= data.videoUrl || '' %>" placeholder="/assets/video/sample.mp4">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-video"
|
||||
data-target-input="videoUrl">
|
||||
<i class="fas fa-upload me-1"></i>Upload Video
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted">MP4 video file path or URL</small>
|
||||
<% if (data.videoUrl) { %>
|
||||
<div class="mt-2">
|
||||
<video width="100%" height="200" controls style="border-radius: 8px;">
|
||||
<source src="<%= data.videoUrl %>" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fixed Bottom Buttons -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
|
||||
<i class="fas fa-undo me-2"></i>Reset
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<i class="fas fa-save me-2"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initializeFormHandlers();
|
||||
});
|
||||
|
||||
function initializeFormHandlers() {
|
||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
const imageType = this.dataset.imageType;
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.btn-upload-video').forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
openVideoUploader(targetInput);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
if (confirm('Are you sure you want to reset all changes?')) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
function openImageUploader(targetInput, imageType) {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.style.display = 'none';
|
||||
document.body.appendChild(fileInput);
|
||||
|
||||
fileInput.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : 'Upload';
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
|
||||
}
|
||||
|
||||
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Upload failed');
|
||||
}
|
||||
|
||||
document.getElementById(targetInput).value = result.path;
|
||||
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
}
|
||||
|
||||
alert('Image uploaded successfully!');
|
||||
location.reload();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
alert('Failed to upload image: ' + error.message);
|
||||
} finally {
|
||||
document.body.removeChild(fileInput);
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.click();
|
||||
}
|
||||
|
||||
function openVideoUploader(targetInput) {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'video/mp4,video/webm,video/ogg';
|
||||
fileInput.style.display = 'none';
|
||||
document.body.appendChild(fileInput);
|
||||
|
||||
fileInput.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('video', file);
|
||||
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : 'Upload';
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
|
||||
}
|
||||
|
||||
const response = await fetch('/admin/upload/video', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Upload failed');
|
||||
}
|
||||
|
||||
document.getElementById(targetInput).value = result.path;
|
||||
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
}
|
||||
|
||||
alert('Video uploaded successfully!');
|
||||
location.reload();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
alert('Failed to upload video: ' + error.message);
|
||||
} finally {
|
||||
document.body.removeChild(fileInput);
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.click();
|
||||
}
|
||||
</script>
|
||||
@@ -1,638 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-2" style="color: var(--primary-dark);">
|
||||
<%= title %>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">
|
||||
Manage insurance page content with hero section, page information, and rich content editor
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-primary preview-btn">
|
||||
<i class="fas fa-eye me-2"></i>Preview
|
||||
</button>
|
||||
<button type="submit" form="insuranceForm" class="btn btn-primary" id="saveBtn">
|
||||
<i class="fas fa-save me-2"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="insuranceForm" action="/admin/insurance/update" method="POST" class="needs-validation" novalidate>
|
||||
<!-- Hidden inputs -->
|
||||
<input type="hidden" name="hero" id="heroJson">
|
||||
<input type="hidden" name="page" id="pageJson">
|
||||
<input type="hidden" name="content" id="contentJson">
|
||||
|
||||
<div class="row">
|
||||
<!-- Main Content Column -->
|
||||
<div class="col-lg-8">
|
||||
<!-- Page Information Card -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="card-title mb-0">Page Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Page Title <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="pageTitle" rows="2" required><%= data.page?.title || 'Insurance & Travel Information' %></textarea>
|
||||
<div class="invalid-feedback">Please enter a page title</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hero Section Card -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="card-title mb-0">Hero Section</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Background Image</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="heroBackgroundImage"
|
||||
value="<%= data.hero?.backgroundImage || '/uploads/banner/b13.jpg' %>" readonly>
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="heroBackgroundImage"
|
||||
data-image-type="insurance">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text text-muted">Recommended size: 1920x1080px</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div id="heroImagePreview" style="height: 200px; width: 100%;">
|
||||
<% if (data.hero?.backgroundImage) { %>
|
||||
<%
|
||||
const heroImgSrc = data.hero.backgroundImage.startsWith('http')
|
||||
? data.hero.backgroundImage
|
||||
: (frontendUrl ? frontendUrl + '/' + data.hero.backgroundImage : data.hero.backgroundImage);
|
||||
%>
|
||||
<img src="<%= heroImgSrc %>" class="img-thumbnail"
|
||||
style="height: 200px; width: 100%; object-fit: cover;"
|
||||
alt="Background image preview"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
||||
<div class="border rounded p-5 text-center text-muted"
|
||||
style="height: 200px; display: none; align-items: center; justify-content: center; flex-direction: column;">
|
||||
<i class="fas fa-image fa-3x mb-2"></i><p class="mb-0">Image preview not available</p>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="border rounded p-5 text-center text-muted"
|
||||
style="height: 200px; display: flex; align-items: center; justify-content: center; flex-direction: column;">
|
||||
<i class="fas fa-image fa-3x mb-2"></i><p class="mb-0">No image selected</p>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Hero Title</label>
|
||||
<input type="text" class="form-control" id="heroTitle"
|
||||
value="<%= data.hero?.title || 'Insurance & Travel Cancellation Guarantee' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Hero Subtitle</label>
|
||||
<input type="text" class="form-control" id="heroSubtitle"
|
||||
value="<%= data.hero?.subtitle || 'Comprehensive coverage for your peace of mind' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Editor Section -->
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="card-title mb-0">Content Editor</h5>
|
||||
<p class="text-muted mb-0 small">Write your insurance page content just like a blog post</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="editorjs" class="border rounded p-3" style="min-height: 500px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Column -->
|
||||
<div class="col-lg-4">
|
||||
<!-- SEO Settings Card -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="card-title mb-0">SEO Settings</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Meta Title</label>
|
||||
<input type="text" class="form-control" id="metadataTitle"
|
||||
value="<%= data.page?.metadata?.title || '' %>">
|
||||
<small class="text-muted">Title for search engines (max 60 characters)</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Meta Description</label>
|
||||
<textarea class="form-control" id="metadataDescription" rows="3"
|
||||
placeholder="Meta description for SEO"><%= data.page?.metadata?.description || '' %></textarea>
|
||||
<small class="text-muted">Description for search engines (max 160 characters)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page Settings Card -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="card-title mb-0">Page Settings</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="pageDivider"
|
||||
<%= data.page?.divider !== false ? 'checked' : '' %>>
|
||||
<label class="form-check-label" for="pageDivider">Show page divider</label>
|
||||
</div>
|
||||
<small class="text-muted">Display divider line below page title</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="enableScrollspy"
|
||||
<%= data.hero?.enableScrollspy ? 'checked' : '' %>>
|
||||
<label class="form-check-label" for="enableScrollspy">Enable Scrollspy Navigation</label>
|
||||
</div>
|
||||
<small class="text-muted">Creates table of contents from headers</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Tips Card -->
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="card-title mb-0">Content Tips</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
<h6><i class="fas fa-lightbulb me-2"></i>Tips for Insurance Content:</h6>
|
||||
<ul class="mb-0 small">
|
||||
<li>Use <strong>Header 2</strong> for main sections</li>
|
||||
<li>Use <strong>Header 3</strong> for subsections</li>
|
||||
<li>Use <strong>Lists</strong> for coverage items</li>
|
||||
<li>Use <strong>Quote</strong> for important notes</li>
|
||||
<li>Use <strong>Embed</strong> for video explanations</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Preview Modal -->
|
||||
<div class="modal fade" id="previewModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Insurance Page Preview</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<iframe id="previewFrame" style="width: 100%; height: 80vh; border: none;"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden inputs for file upload -->
|
||||
<input type="file" id="directImageUpload" style="display: none;">
|
||||
<input type="hidden" id="currentImageType">
|
||||
<input type="hidden" id="currentTargetInput">
|
||||
|
||||
<!-- Editor.js Dependencies -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2.28.2"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@2.7.0"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@2.11.3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/list@1.8.0"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/image@2.8.1"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@2.5.0"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@1.3.0"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@2.5.3"></script>
|
||||
|
||||
<script>
|
||||
// Insurance Content Manager
|
||||
class InsuranceContentManager {
|
||||
// Convert Editor.js data to Insurance content structure
|
||||
convertEditorToInsurance(editorData) {
|
||||
const contentItems = [];
|
||||
|
||||
editorData.blocks.forEach(block => {
|
||||
switch (block.type) {
|
||||
case 'header':
|
||||
contentItems.push({
|
||||
type: 'header',
|
||||
level: block.data.level || 2,
|
||||
text: block.data.text || ''
|
||||
});
|
||||
break;
|
||||
|
||||
case 'paragraph':
|
||||
contentItems.push({
|
||||
type: 'paragraph',
|
||||
text: block.data.text || ''
|
||||
});
|
||||
break;
|
||||
|
||||
case 'list':
|
||||
contentItems.push({
|
||||
type: 'list',
|
||||
level: block.data.style === 'ordered' ? 1 : 0, // 1 for ordered, 0 for unordered
|
||||
items: block.data.items || []
|
||||
});
|
||||
break;
|
||||
|
||||
case 'quote':
|
||||
contentItems.push({
|
||||
type: 'note',
|
||||
text: block.data.text || '',
|
||||
caption: block.data.caption || ''
|
||||
});
|
||||
break;
|
||||
|
||||
case 'embed':
|
||||
contentItems.push({
|
||||
type: 'embed',
|
||||
source: block.data.service || 'youtube',
|
||||
url: block.data.embed || '',
|
||||
embed: block.data.embed || '',
|
||||
caption: block.data.caption || '',
|
||||
width: block.data.width || 560,
|
||||
height: block.data.height || 315
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return contentItems;
|
||||
}
|
||||
|
||||
// Convert Insurance content to Editor.js data
|
||||
convertInsuranceToEditor(insuranceContent) {
|
||||
const blocks = [];
|
||||
|
||||
insuranceContent.forEach(item => {
|
||||
switch (item.type) {
|
||||
case 'header':
|
||||
blocks.push({
|
||||
type: 'header',
|
||||
data: {
|
||||
text: item.text || '',
|
||||
level: item.level || 2
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'paragraph':
|
||||
blocks.push({
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: item.text || ''
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'section':
|
||||
if (item.title) {
|
||||
blocks.push({
|
||||
type: 'header',
|
||||
data: {
|
||||
text: item.title,
|
||||
level: 3
|
||||
}
|
||||
});
|
||||
}
|
||||
if (item.content) {
|
||||
blocks.push({
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: item.content
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'list':
|
||||
blocks.push({
|
||||
type: 'list',
|
||||
data: {
|
||||
style: item.level === 1 ? 'ordered' : 'unordered', // Check level to determine style
|
||||
items: item.items || []
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'note':
|
||||
blocks.push({
|
||||
type: 'quote',
|
||||
data: {
|
||||
text: item.text || '',
|
||||
caption: item.caption || ''
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'embed':
|
||||
blocks.push({
|
||||
type: 'embed',
|
||||
data: {
|
||||
service: item.source || 'youtube',
|
||||
embed: item.url || item.embed || '',
|
||||
caption: item.caption || '',
|
||||
width: item.width || 560,
|
||||
height: item.height || 315
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
time: Date.now(),
|
||||
blocks: blocks,
|
||||
version: "2.28.2"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Editor
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Initialize content manager
|
||||
window.insuranceContentManager = new InsuranceContentManager();
|
||||
|
||||
// Load existing content if available
|
||||
let initialEditorData = {
|
||||
time: Date.now(),
|
||||
blocks: [],
|
||||
version: "2.28.2"
|
||||
};
|
||||
|
||||
if (window.INSURANCE_DATA?.content?.content) {
|
||||
try {
|
||||
const insuranceContent = window.INSURANCE_DATA.content.content;
|
||||
initialEditorData = window.insuranceContentManager.convertInsuranceToEditor(insuranceContent);
|
||||
} catch (error) {
|
||||
console.error('Error loading insurance content:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Editor via BlogEditor module (adds uploader + autosave)
|
||||
let blogEditorInstance = null;
|
||||
try {
|
||||
if (window.BlogEditor) {
|
||||
const initialCustom = window.INSURANCE_DATA?.content?.content ? { blocks: window.INSURANCE_DATA.content.content } : { blocks: [] };
|
||||
blogEditorInstance = new window.BlogEditor('editorjs', initialCustom);
|
||||
window.blogEditorInstance = blogEditorInstance;
|
||||
console.log('BlogEditor instance created for insurance');
|
||||
} else {
|
||||
console.warn('BlogEditor not available. Falling back to EditorJS init.');
|
||||
window.insuranceEditor = new EditorJS({
|
||||
holder: 'editorjs',
|
||||
data: initialEditorData,
|
||||
tools: {
|
||||
header: Header,
|
||||
paragraph: {
|
||||
class: Paragraph,
|
||||
inlineToolbar: true
|
||||
},
|
||||
list: List,
|
||||
quote: Quote,
|
||||
marker: Marker,
|
||||
embed: Embed
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error initializing editor module:', err);
|
||||
window.insuranceEditor = new EditorJS({ holder: 'editorjs', data: initialEditorData });
|
||||
}
|
||||
|
||||
// Form submission handler
|
||||
const form = document.getElementById('insuranceForm');
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const saveBtn = document.getElementById('saveBtn');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
|
||||
|
||||
try {
|
||||
// Save editor content
|
||||
let editorData = null;
|
||||
if (window.blogEditorInstance?.editor) {
|
||||
editorData = await window.blogEditorInstance.editor.save();
|
||||
} else if (window.insuranceEditor) {
|
||||
editorData = await window.insuranceEditor.save();
|
||||
} else {
|
||||
throw new Error('Editor not initialized');
|
||||
}
|
||||
|
||||
// Convert to Insurance content structure
|
||||
const insuranceContent = window.insuranceContentManager.convertEditorToInsurance(editorData);
|
||||
|
||||
// Prepare JSON data
|
||||
const heroData = {
|
||||
title: document.getElementById('heroTitle').value,
|
||||
subtitle: document.getElementById('heroSubtitle').value,
|
||||
backgroundImage: document.getElementById('heroBackgroundImage').value,
|
||||
sectionClass: "uk-section-default uk-section-overlap uk-preserve-color uk-light uk-position-relative",
|
||||
backgroundClasses: "uk-background-norepeat uk-background-cover uk-background-top-center uk-section uk-section-xlarge",
|
||||
overlayStyle: { backgroundColor: "rgba(0, 0, 0, 0)" },
|
||||
titleClass: "text-white text-[5vw] uk-text-center",
|
||||
subtitleClass: "uk-panel font-[Raleway] italic text-[1.5vw] uk-margin uk-text-center",
|
||||
enableScrollspy: true
|
||||
};
|
||||
|
||||
const pageData = {
|
||||
title: document.getElementById('pageTitle').value,
|
||||
divider: document.getElementById('pageDivider').checked,
|
||||
sectionClass: "uk-section-default uk-section-overlap uk-section",
|
||||
titleClass: "text-[2.5vw] text-[#292c3d] uk-text-left@m uk-text-center",
|
||||
dividerClass: "uk-divider-small uk-text-left@m uk-text-center"
|
||||
};
|
||||
|
||||
const contentData = {
|
||||
sectionClass: "uk-section-muted uk-section-overlap uk-section",
|
||||
textClass: "uk-panel uk-margin text-[1vw]",
|
||||
content: insuranceContent
|
||||
};
|
||||
|
||||
// Set hidden inputs
|
||||
document.getElementById('heroJson').value = JSON.stringify(heroData);
|
||||
document.getElementById('pageJson').value = JSON.stringify(pageData);
|
||||
document.getElementById('contentJson').value = JSON.stringify(contentData);
|
||||
|
||||
// Submit form
|
||||
form.submit();
|
||||
} catch (error) {
|
||||
console.error('Error saving insurance content:', error);
|
||||
alert('Error saving content. Please check console for details.');
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
|
||||
}
|
||||
});
|
||||
|
||||
// Preview functionality
|
||||
const previewBtn = document.querySelector('.preview-btn');
|
||||
const previewModal = new bootstrap.Modal(document.getElementById('previewModal'));
|
||||
|
||||
previewBtn.addEventListener('click', async function () {
|
||||
try {
|
||||
// Save editor content first
|
||||
let editorData = null;
|
||||
if (window.blogEditorInstance?.editor) {
|
||||
editorData = await window.blogEditorInstance.editor.save();
|
||||
} else if (window.insuranceEditor) {
|
||||
editorData = await window.insuranceEditor.save();
|
||||
} else {
|
||||
throw new Error('Editor not initialized');
|
||||
}
|
||||
const insuranceContent = window.insuranceContentManager.convertEditorToInsurance(editorData);
|
||||
|
||||
// Prepare data for preview
|
||||
const heroData = {
|
||||
title: document.getElementById('heroTitle').value,
|
||||
subtitle: document.getElementById('heroSubtitle').value,
|
||||
backgroundImage: document.getElementById('heroBackgroundImage').value
|
||||
};
|
||||
|
||||
const pageData = {
|
||||
title: document.getElementById('pageTitle').value,
|
||||
divider: document.getElementById('pageDivider').checked
|
||||
};
|
||||
|
||||
const contentData = {
|
||||
content: insuranceContent
|
||||
};
|
||||
|
||||
const response = await fetch('/admin/insurance/preview', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
hero: JSON.stringify(heroData),
|
||||
page: JSON.stringify(pageData),
|
||||
content: JSON.stringify(contentData)
|
||||
})
|
||||
});
|
||||
|
||||
const html = await response.text();
|
||||
const previewFrame = document.getElementById('previewFrame');
|
||||
previewFrame.srcdoc = html;
|
||||
|
||||
previewModal.show();
|
||||
} catch (error) {
|
||||
console.error('Error generating preview:', error);
|
||||
alert('Error generating preview. Please try again.');
|
||||
}
|
||||
});
|
||||
|
||||
// Image upload handling
|
||||
document.querySelectorAll('.btn-upload-image').forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
const targetInput = this.getAttribute('data-target-input');
|
||||
const imageType = this.getAttribute('data-image-type');
|
||||
document.getElementById('currentImageType').value = imageType;
|
||||
document.getElementById('currentTargetInput').value = targetInput;
|
||||
document.getElementById('directImageUpload').click();
|
||||
});
|
||||
});
|
||||
|
||||
// Handle file selection
|
||||
document.getElementById('directImageUpload').addEventListener('change', async function (e) {
|
||||
if (!this.files || !this.files[0]) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', this.files[0]);
|
||||
const imageType = document.getElementById('currentImageType').value;
|
||||
const targetInput = document.getElementById('currentTargetInput').value;
|
||||
|
||||
try {
|
||||
showToast('Uploading image...', 'info');
|
||||
|
||||
const uploadResponse = await fetch(`/admin/upload/image?imageType=${imageType}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const uploadResult = await uploadResponse.json();
|
||||
|
||||
if (uploadResult.success && uploadResult.path) {
|
||||
const inputElement = document.getElementById(targetInput);
|
||||
if (inputElement) {
|
||||
inputElement.value = uploadResult.path;
|
||||
|
||||
// Update preview
|
||||
if (targetInput === 'heroBackgroundImage') {
|
||||
updateHeroImagePreview(uploadResult.path);
|
||||
}
|
||||
|
||||
// Show success message
|
||||
showToast('Image uploaded successfully', 'success');
|
||||
}
|
||||
} else {
|
||||
showToast(uploadResult.error || 'Error uploading image', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
showToast('Error uploading image', 'error');
|
||||
}
|
||||
|
||||
this.value = '';
|
||||
});
|
||||
|
||||
function updateHeroImagePreview(imageUrl) {
|
||||
const previewDiv = document.getElementById('heroImagePreview');
|
||||
let img = previewDiv.querySelector('img');
|
||||
let fallback = previewDiv.querySelector('.border');
|
||||
|
||||
if (!img) {
|
||||
img = document.createElement('img');
|
||||
img.className = 'img-thumbnail';
|
||||
img.style.height = '200px';
|
||||
img.style.width = '100%';
|
||||
img.style.objectFit = 'cover';
|
||||
img.alt = 'Background image preview';
|
||||
img.onerror = function() {
|
||||
this.style.display = 'none';
|
||||
if (fallback) fallback.style.display = 'flex';
|
||||
};
|
||||
previewDiv.insertBefore(img, fallback);
|
||||
}
|
||||
|
||||
img.src = imageUrl;
|
||||
img.style.display = 'block';
|
||||
if (fallback) fallback.style.display = 'none';
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast align-items-center text-bg-${type} border-0 position-fixed bottom-0 end-0 m-3`;
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">${message}</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
const bsToast = new bootstrap.Toast(toast, { delay: 3000 });
|
||||
bsToast.show();
|
||||
|
||||
toast.addEventListener('hidden.bs.toast', () => {
|
||||
toast.remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Store insurance data in global variable
|
||||
window.INSURANCE_DATA = <%- JSON.stringify(data) %>;
|
||||
</script>
|
||||
@@ -1,743 +1,110 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
|
||||
<% const formattedType = currentType.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); %>
|
||||
<%= formattedType %> Level
|
||||
</h1>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="levelTypeDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<%= formattedType %>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="levelTypeDropdown">
|
||||
<% levelTypes.forEach(type => { %>
|
||||
<li>
|
||||
<a class="dropdown-item <%= type === currentType ? 'active' : '' %>" href="/admin/level?type=<%= type %>">
|
||||
<%= type.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') %>
|
||||
</a>
|
||||
</li>
|
||||
<% }); %>
|
||||
</ul>
|
||||
<!-- Page Header -->
|
||||
<div class="page-title-area">
|
||||
<div>
|
||||
<h1>Level Management</h1>
|
||||
<p class="subtitle">All degree levels registered in the system</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Add Form -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0">
|
||||
<div class="card-header">
|
||||
<h5 class="card-header-title"><i class="fas fa-plus-circle"></i> Add Level</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/admin/level/create">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Level Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="type" placeholder="e.g. Undergraduate, Postgraduate..." required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="fas fa-plus"></i> Add Level
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Thêm nút tạo mới type -->
|
||||
<a href="/admin/level/create" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Add New Level Type
|
||||
</a>
|
||||
|
||||
<!-- Nút xóa type hiện tại (chỉ hiển thị với non-default types) -->
|
||||
<% if (!['undergraduate', 'postgraduate', 'pre-university'].includes(currentType)) { %>
|
||||
<button type="button" class="btn btn-outline-danger" data-custom-modal="open" data-type="<%= currentType %>">
|
||||
<i class="fas fa-trash me-1"></i>Delete This Level Type
|
||||
</button>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form id="levelForm" action="/admin/level/update?type=<%= currentType %>" method="POST" class="content-with-fixed-buttons">
|
||||
<input type="hidden" name="type" value="<%= currentType %>">
|
||||
<input type="hidden" name="banner" id="bannerJson">
|
||||
<input type="hidden" name="overview" id="overviewJson">
|
||||
<input type="hidden" name="requirements" id="requirementsJson">
|
||||
<input type="hidden" name="action_buttons" id="actionbuttonsJson">
|
||||
<input type="hidden" name="why_study" id="whystudyJson">
|
||||
|
||||
<!-- Level Type and Brochure Field -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Level Type</label>
|
||||
<input type="text" class="form-control" value="<%= currentType.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') %>" readonly>
|
||||
<div class="form-text text-muted">This is the level type identifier. It cannot be changed.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Brochure Link</label>
|
||||
<input type="text" class="form-control" id="levelBrochure" name="brochure" value="<%= data.brochure || '#' %>" placeholder="Enter brochure URL or # for no link">
|
||||
<div class="form-text text-muted">Link to the level brochure. Use # if no brochure is available.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#banner" role="tab">
|
||||
<i class="fas fa-image me-2"></i>Banner
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#overview" role="tab">
|
||||
<i class="fas fa-info-circle me-2"></i>Overview
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#requirements" role="tab">
|
||||
<i class="fas fa-list-check me-2"></i>Requirements
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#actions" role="tab">
|
||||
<i class="fas fa-link me-2"></i>Action Buttons
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#why" role="tab">
|
||||
<i class="fas fa-question-circle me-2"></i>Why Study
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<!-- Banner Tab -->
|
||||
<div class="tab-pane fade show active" id="banner" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Banner Image</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6 d-flex align-items-baseline">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="bannerImage" value="<%= data.banner.image %>">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="bannerImage">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
<!-- Table -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card border-0">
|
||||
<div class="card-header">
|
||||
<h5 class="card-header-title"><i class="fas fa-layer-group"></i> Levels</h5>
|
||||
<span class="badge badge-soft-primary"><%= levels ? levels.length : 0 %> total</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<% if (levels && levels.length > 0) { %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:50px">#</th>
|
||||
<th>Level Name</th>
|
||||
<th style="width:130px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% levels.forEach((level, index) => { %>
|
||||
<tr>
|
||||
<td style="color:var(--text-muted)"><%= index + 1 %></td>
|
||||
<td>
|
||||
<span class="level-type-display-<%= level._id %>" style="font-weight:500"><%= level.type %></span>
|
||||
<form method="POST" action="/admin/level/<%= level._id %>/edit"
|
||||
class="level-edit-form-<%= level._id %> d-none d-flex gap-2 align-items-center">
|
||||
<input type="text" class="form-control form-control-sm" name="type" value="<%= level.type %>" required>
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary btn-icon" title="Save">
|
||||
<i class="fas fa-check"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary btn-icon" title="Cancel"
|
||||
onclick="cancelLevelEdit('<%= level._id %>')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-actions">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary btn-icon" title="Edit"
|
||||
onclick="startLevelEdit('<%= level._id %>')">
|
||||
<i class="fas fa-pen"></i>
|
||||
</button>
|
||||
<form method="POST" action="/admin/level/<%= level._id %>/delete" style="display:inline;"
|
||||
onsubmit="return confirm('Delete level?')">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger btn-icon" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<!-- Ảnh preview cho banner -->
|
||||
<div class="image-preview mt-2">
|
||||
<% if (data.banner.image) { %>
|
||||
<img src="<%= data.banner.image %>" alt="Banner preview" class="img-thumbnail" style="height:200px;width:100%;object-fit:cover;">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="bannerTitle" value="<%= data.banner.title %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Text</label>
|
||||
<textarea class="form-control" id="bannerText" rows="3"><%= data.banner.text %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overview Tab -->
|
||||
<div class="tab-pane fade" id="overview" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="overviewTitle" value="<%= data.overview.title %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Paragraphs</label>
|
||||
<div id="overviewParagraphs">
|
||||
<% (data.overview.paragraphs || []).forEach((paragraph, index) => { %>
|
||||
<div class="mb-3 d-flex gap-2">
|
||||
<textarea class="form-control" rows="3"><%= paragraph %></textarea>
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removeParagraph(this)">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary mt-2" onclick="addParagraph('overviewParagraphs')">
|
||||
<i class="fas fa-plus me-1"></i>Add Paragraph
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Contact Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="contactTitle" value="<%= data.overview.contact_info.title %>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Subtitle</label>
|
||||
<input type="text" class="form-control" id="contactSubtitle" value="<%= data.overview.contact_info.subtitle %>">
|
||||
</div>
|
||||
<div id="contactItems">
|
||||
<% (data.overview.contact_info.items || []).forEach((item, index) => { %>
|
||||
<div class="mb-3">
|
||||
<input type="text" class="form-control" value="<%= item.text %>">
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Info -->
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Social Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="socialTitle" value="<%= data.overview.social_info.title %>">
|
||||
</div>
|
||||
<div id="socialLinks">
|
||||
<% (data.overview.social_info.social_links || []).forEach((link, index) => { %>
|
||||
<div class="mb-3 border p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Image</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" value="<%= link.image %>">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">URL</label>
|
||||
<input type="text" class="form-control" value="<%= link.url %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Alt Text</label>
|
||||
<input type="text" class="form-control" value="<%= link.alt %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h6>Apply Button</h6>
|
||||
<div class="row g-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Text</label>
|
||||
<input type="text" class="form-control" id="applyButtonText" value="<%= data.overview.social_info.apply_button.text %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">URL</label>
|
||||
<input type="text" class="form-control" id="applyButtonUrl" value="<%= data.overview.social_info.apply_button.url %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Requirements Tab -->
|
||||
<div class="tab-pane fade" id="requirements" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="requirementsTitle" value="<%= data.requirements.title %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Requirements List</label>
|
||||
<div id="requirementItems">
|
||||
<% (data.requirements.items || []).forEach((item, index) => { %>
|
||||
<div class="mb-3 d-flex gap-2">
|
||||
<input type="text" class="form-control" value="<%= item %>">
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removeItem(this)">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary mt-2" onclick="addRequirementItem()">
|
||||
<i class="fas fa-plus me-1"></i>Add Requirement
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons Tab -->
|
||||
<div class="tab-pane fade" id="actions" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="actionButtonsTitle" value="<%= data.action_buttons.title %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Buttons</label>
|
||||
<div id="actionButtonsList">
|
||||
<% (data.action_buttons.buttons || []).forEach((button, index) => { %>
|
||||
<div class="mb-3 border p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">Text</label>
|
||||
<input type="text" class="form-control" value="<%= button.text %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Link</label>
|
||||
<input type="text" class="form-control" value="<%= button.link %>">
|
||||
</div>
|
||||
<div class="col-md-1 d-flex align-items-end">
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removeActionButton(this)">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary mt-2" onclick="addActionButton()">
|
||||
<i class="fas fa-plus me-1"></i>Add Button
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Why Study Tab -->
|
||||
<div class="tab-pane fade" id="why" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="whyStudyTitle" value="<%= data.why_study.title %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Items</label>
|
||||
<div id="whyStudyItems">
|
||||
<% (data.why_study.items || []).forEach((item, index) => { %>
|
||||
<div class="mb-3 border p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Number</label>
|
||||
<input type="text" class="form-control" value="<%= item.number %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" value="<%= item.title %>">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">Text</label>
|
||||
<textarea class="form-control" rows="2"><%= item.text %></textarea>
|
||||
</div>
|
||||
<div class="col-md-1 d-flex align-items-end">
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removeWhyStudyItem(this)">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary mt-2" onclick="addWhyStudyItem()">
|
||||
<i class="fas fa-plus me-1"></i>Add Item
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fixed bottom buttons -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary">
|
||||
<i class="fas fa-undo me-1"></i>Reset
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<% } else { %>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon"><i class="fas fa-layer-group"></i></div>
|
||||
<h5>No levels yet</h5>
|
||||
<p>Use the form on the left to add the first level.</p>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Thêm input file ẩn để sử dụng cho upload trực tiếp -->
|
||||
<input type="file" id="directImageUpload" style="display: none;">
|
||||
<input type="hidden" id="currentImageType" name="imageType">
|
||||
<input type="hidden" id="currentTargetInput" name="targetInput">
|
||||
|
||||
<script>
|
||||
// Helper function to create JSON from form data with validation
|
||||
function createJsonFromForm() {
|
||||
try {
|
||||
// Banner
|
||||
const bannerJson = {
|
||||
image: document.getElementById('bannerImage')?.value || '',
|
||||
title: document.getElementById('bannerTitle')?.value || '',
|
||||
text: document.getElementById('bannerText')?.value || ''
|
||||
};
|
||||
document.getElementById('bannerJson').value = JSON.stringify(bannerJson);
|
||||
|
||||
// Overview
|
||||
const overviewJson = {
|
||||
title: document.getElementById('overviewTitle')?.value || '',
|
||||
paragraphs: Array.from(document.querySelectorAll('#overviewParagraphs textarea')).map(ta => ta.value || ''),
|
||||
contact_info: {
|
||||
title: document.getElementById('contactTitle')?.value || '',
|
||||
subtitle: document.getElementById('contactSubtitle')?.value || '',
|
||||
items: Array.from(document.querySelectorAll('#contactItems input')).map(input => ({
|
||||
text: input.value || ''
|
||||
}))
|
||||
},
|
||||
social_info: {
|
||||
title: document.getElementById('socialTitle')?.value || '',
|
||||
social_links: Array.from(document.querySelectorAll('#socialLinks .mb-3')).map(div => ({
|
||||
image: div.querySelectorAll('input')[0]?.value || '',
|
||||
url: div.querySelectorAll('input')[1]?.value || '',
|
||||
alt: div.querySelectorAll('input')[2]?.value || ''
|
||||
})),
|
||||
apply_button: {
|
||||
text: document.getElementById('applyButtonText')?.value || '',
|
||||
url: document.getElementById('applyButtonUrl')?.value || ''
|
||||
}
|
||||
}
|
||||
};
|
||||
document.getElementById('overviewJson').value = JSON.stringify(overviewJson);
|
||||
|
||||
// Requirements
|
||||
const requirementsJson = {
|
||||
title: document.getElementById('requirementsTitle')?.value || '',
|
||||
items: Array.from(document.querySelectorAll('#requirementItems input')).map(input => input.value || '')
|
||||
};
|
||||
document.getElementById('requirementsJson').value = JSON.stringify(requirementsJson);
|
||||
|
||||
// Action Buttons
|
||||
const actionButtonsJson = {
|
||||
title: document.getElementById('actionButtonsTitle')?.value || '',
|
||||
buttons: Array.from(document.querySelectorAll('#actionButtonsList .mb-3')).map(div => ({
|
||||
text: div.querySelectorAll('input')[0]?.value || '',
|
||||
link: div.querySelectorAll('input')[1]?.value || ''
|
||||
}))
|
||||
};
|
||||
document.getElementById('actionbuttonsJson').value = JSON.stringify(actionButtonsJson);
|
||||
|
||||
// Why Study
|
||||
const whyStudyJson = {
|
||||
title: document.getElementById('whyStudyTitle')?.value || '',
|
||||
items: Array.from(document.querySelectorAll('#whyStudyItems .mb-3')).map(div => ({
|
||||
number: div.querySelectorAll('input')[0]?.value || '',
|
||||
title: div.querySelectorAll('input')[1]?.value || '',
|
||||
text: div.querySelector('textarea')?.value || ''
|
||||
}))
|
||||
};
|
||||
document.getElementById('whystudyJson').value = JSON.stringify(whyStudyJson);
|
||||
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error creating JSON:', error);
|
||||
if (window.toastManager) {
|
||||
window.toastManager.error('Error preparing form data: ' + error.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function startLevelEdit(id) {
|
||||
document.querySelector('.level-type-display-' + id).classList.add('d-none');
|
||||
const form = document.querySelector('.level-edit-form-' + id);
|
||||
form.classList.remove('d-none');
|
||||
form.classList.add('d-flex');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Xử lý upload hình ảnh
|
||||
document.querySelectorAll('.btn-upload-image').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const targetInput = this.getAttribute('data-target-input');
|
||||
const imageType = this.getAttribute('data-image-type') || 'level';
|
||||
|
||||
// Lưu thông tin vào hidden inputs
|
||||
document.getElementById('currentImageType').value = imageType;
|
||||
document.getElementById('currentTargetInput').value = targetInput;
|
||||
|
||||
// Kích hoạt input file ẩn
|
||||
document.getElementById('directImageUpload').click();
|
||||
});
|
||||
});
|
||||
|
||||
// Xử lý khi chọn file
|
||||
document.getElementById('directImageUpload').addEventListener('change', async function(e) {
|
||||
if (!this.files || !this.files[0]) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', this.files[0]);
|
||||
const imageType = document.getElementById('currentImageType').value;
|
||||
const targetInput = document.getElementById('currentTargetInput').value;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.path) {
|
||||
const inputElement = document.getElementById(targetInput);
|
||||
if (inputElement) {
|
||||
inputElement.value = result.path;
|
||||
|
||||
// Cập nhật hoặc tạo mới hình ảnh preview
|
||||
let previewContainer = inputElement.closest('.input-group').parentElement.querySelector('.image-preview');
|
||||
|
||||
// Tìm container image-preview trong cùng hàng nhưng ở cột bên cạnh
|
||||
if (!previewContainer) {
|
||||
previewContainer = inputElement.closest('.row').querySelector('.col-md-6:nth-child(2) .image-preview');
|
||||
}
|
||||
|
||||
// Nếu chưa có container preview thì tạo mới
|
||||
if (!previewContainer) {
|
||||
previewContainer = document.createElement('div');
|
||||
previewContainer.className = 'image-preview mt-2';
|
||||
const rightColumn = inputElement.closest('.row').querySelector('.col-md-6:nth-child(2)');
|
||||
if (rightColumn) {
|
||||
rightColumn.appendChild(previewContainer);
|
||||
} else {
|
||||
// Fallback: tạo tại cột hiện tại nếu không tìm thấy cột bên phải
|
||||
previewContainer = document.createElement('div');
|
||||
previewContainer.className = 'image-preview mt-2';
|
||||
inputElement.closest('.input-group').parentElement.appendChild(previewContainer);
|
||||
}
|
||||
}
|
||||
|
||||
// Tìm hoặc tạo mới thẻ img để preview ảnh
|
||||
let img = previewContainer.querySelector('img');
|
||||
if (!img) {
|
||||
img = document.createElement('img');
|
||||
img.className = 'img-thumbnail';
|
||||
previewContainer.appendChild(img);
|
||||
}
|
||||
|
||||
// Thiết lập style cho ảnh preview dựa vào loại ảnh
|
||||
if (targetInput.toLowerCase().includes('logo')) {
|
||||
img.style.height = '100px';
|
||||
img.style.objectFit = 'contain';
|
||||
img.style.width = '';
|
||||
} else {
|
||||
img.style.height = '200px';
|
||||
img.style.width = '100%';
|
||||
img.style.objectFit = 'cover';
|
||||
}
|
||||
|
||||
img.src = result.path;
|
||||
img.alt = 'Image preview';
|
||||
|
||||
if (window.toastManager) {
|
||||
window.toastManager.success('Tải ảnh thành công');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (window.toastManager) {
|
||||
window.toastManager.error(result.error || 'Error uploading image');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
if (window.toastManager) {
|
||||
window.toastManager.error('Error uploading image');
|
||||
}
|
||||
}
|
||||
|
||||
// Reset input để có thể chọn lại cùng một file nếu cần
|
||||
this.value = '';
|
||||
});
|
||||
});
|
||||
|
||||
// Form submit handler with validation
|
||||
document.getElementById('levelForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
// Create and validate JSON data
|
||||
if (!createJsonFromForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
const requiredFields = ['bannerJson', 'overviewJson', 'requirementsJson'];
|
||||
const missingFields = requiredFields.filter(field => {
|
||||
const value = document.getElementById(field).value;
|
||||
return !value || value === '{}';
|
||||
});
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
if (window.toastManager) {
|
||||
window.toastManager.error(`Missing required data: ${missingFields.join(', ')}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading toast
|
||||
if (window.toastManager) {
|
||||
window.toastManager.info('Saving changes...');
|
||||
}
|
||||
|
||||
this.submit();
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
if (window.toastManager) {
|
||||
window.toastManager.error('Error submitting form: ' + error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Helper functions for adding/removing items
|
||||
function addParagraph(containerId) {
|
||||
const container = document.getElementById(containerId);
|
||||
const div = document.createElement('div');
|
||||
div.className = 'mb-3 d-flex gap-2';
|
||||
div.innerHTML = `
|
||||
<textarea class="form-control" rows="3"></textarea>
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removeParagraph(this)">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
function cancelLevelEdit(id) {
|
||||
document.querySelector('.level-type-display-' + id).classList.remove('d-none');
|
||||
const form = document.querySelector('.level-edit-form-' + id);
|
||||
form.classList.add('d-none');
|
||||
form.classList.remove('d-flex');
|
||||
}
|
||||
|
||||
function removeParagraph(button) {
|
||||
button.closest('.mb-3').remove();
|
||||
}
|
||||
|
||||
function addRequirementItem() {
|
||||
const container = document.getElementById('requirementItems');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'mb-3 d-flex gap-2';
|
||||
div.innerHTML = `
|
||||
<input type="text" class="form-control">
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removeItem(this)">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
function removeItem(button) {
|
||||
button.closest('.mb-3').remove();
|
||||
}
|
||||
|
||||
function addActionButton() {
|
||||
const container = document.getElementById('actionButtonsList');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'mb-3 border p-3';
|
||||
div.innerHTML = `
|
||||
<div class="row g-2">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">Text</label>
|
||||
<input type="text" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Link</label>
|
||||
<input type="text" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-1 d-flex align-items-end">
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removeActionButton(this)">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
function removeActionButton(button) {
|
||||
button.closest('.mb-3').remove();
|
||||
}
|
||||
|
||||
function addWhyStudyItem() {
|
||||
const container = document.getElementById('whyStudyItems');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'mb-3 border p-3';
|
||||
div.innerHTML = `
|
||||
<div class="row g-2">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Number</label>
|
||||
<input type="text" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">Text</label>
|
||||
<textarea class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="col-md-1 d-flex align-items-end">
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removeWhyStudyItem(this)">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
function removeWhyStudyItem(button) {
|
||||
button.closest('.mb-3').remove();
|
||||
}
|
||||
|
||||
// (Removed) Helper functions for form/apply/application as Level now references Form only
|
||||
|
||||
// Tận dụng module ImageUploader đã được tải trong layout
|
||||
</script>
|
||||
|
||||
<!-- Modal tùy chỉnh xác nhận xóa level type -->
|
||||
<div id="customModal" class="custom-modal">
|
||||
<div class="custom-modal-content">
|
||||
<div class="custom-modal-header">
|
||||
<h5 class="custom-modal-title">Delete Confirmation</h5>
|
||||
<button type="button" class="custom-modal-close">×</button>
|
||||
</div>
|
||||
<div class="custom-modal-body">
|
||||
<p id="modalMessage">Are you sure you want to delete this level type?</p>
|
||||
</div>
|
||||
<div class="custom-modal-footer">
|
||||
<button type="button" class="btn btn-secondary custom-modal-cancel">Cancel</button>
|
||||
<button type="button" class="btn btn-danger custom-modal-ok">Delete Permanently</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import custom modal CSS -->
|
||||
<link rel="stylesheet" href="/css/custom-modal.css">
|
||||
|
||||
<script>
|
||||
// Custom Modal Functions
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Khởi tạo modal tùy chỉnh
|
||||
CustomModal.init('customModal', {
|
||||
closeOnOutsideClick: true,
|
||||
animationDuration: 300
|
||||
});
|
||||
|
||||
// Mở modal
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.getAttribute('data-custom-modal') === 'open' ||
|
||||
e.target.parentElement.getAttribute('data-custom-modal') === 'open') {
|
||||
|
||||
// Lấy button hoặc icon parent nếu click vào icon
|
||||
const button = e.target.getAttribute('data-custom-modal') === 'open' ?
|
||||
e.target : e.target.parentElement;
|
||||
|
||||
const type = button.getAttribute('data-type') || '<%= currentType %>';
|
||||
|
||||
// Sử dụng CustomModal.confirm thay vì xử lý trực tiếp
|
||||
CustomModal.confirm(
|
||||
`Are you sure you want to delete level type "${type}"? This action cannot be undone. All related data will be permanently deleted.`,
|
||||
function() {
|
||||
// Hành động khi xác nhận
|
||||
window.location.href = `/admin/level/delete/${type}`;
|
||||
},
|
||||
null, // Không cần xử lý khi hủy
|
||||
'Delete Confirmation'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,742 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
|
||||
<%= title %>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Edit content displayed on Pricing page</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="<%= frontendUrl %>/pricing/" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-2"></i>View Pricing Page
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form method="POST" class="content-with-fixed-buttons" id="pricingForm" action="/admin/pricing/update">
|
||||
<!-- Hidden inputs for JSON data -->
|
||||
<input type="hidden" name="hero" id="heroJson">
|
||||
<input type="hidden" name="pricingSection" id="pricingSectionJson">
|
||||
<input type="hidden" name="plans" id="plansJson">
|
||||
<input type="hidden" name="testimonials" id="testimonialsJson">
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#hero" role="tab">
|
||||
<i class="fas fa-home me-2"></i>Hero
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#pricingSection" role="tab">
|
||||
<i class="fas fa-tags me-2"></i>Pricing Section
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#plans" role="tab">
|
||||
<i class="fas fa-dollar-sign me-2"></i>Plans
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#testimonials" role="tab">
|
||||
<i class="fas fa-quote-right me-2"></i>Testimonials
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<!-- Hero Tab -->
|
||||
<div class="tab-pane fade show active" id="hero" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-body">
|
||||
<h6 class="fw-medium mb-3">Hero Section</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label fw-medium">Background Image</label>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="heroBackgroundImage"
|
||||
name="heroBackgroundImage"
|
||||
value="<%= data.hero?.backgroundImage || '' %>">
|
||||
<button type="button"
|
||||
class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="heroBackgroundImage"
|
||||
data-image-type="pricing">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted">Recommended size: 1920x1080px</small>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div id="heroImagePreview" style="height: 200px;">
|
||||
<% if (data.hero?.backgroundImage) { %>
|
||||
<% let heroImgSrc=data.hero.backgroundImage; if (heroImgSrc &&
|
||||
!heroImgSrc.startsWith('http://') &&
|
||||
!heroImgSrc.startsWith('https://')) {
|
||||
heroImgSrc=heroImgSrc.startsWith('/') ? heroImgSrc : '/' +
|
||||
heroImgSrc; } %>
|
||||
<img src="<%= heroImgSrc %>" class="img-thumbnail"
|
||||
id="heroPreviewImg"
|
||||
style="height: 200px; width: 100%; object-fit: cover;"
|
||||
alt="Background image preview"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
||||
<div class="border rounded p-5 text-center text-muted"
|
||||
style="height: 200px; display: none; align-items: center; justify-content: center;">
|
||||
Image preview
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="border rounded p-5 text-center text-muted"
|
||||
style="height: 200px; display: flex; align-items: center; justify-content: center;">
|
||||
Image preview
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<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 || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pricing Section Tab -->
|
||||
<div class="tab-pane fade" id="pricingSection" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-body">
|
||||
<h6 class="fw-medium mb-3">Pricing Section Header</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subtitle</label>
|
||||
<input type="text" class="form-control" id="pricingSectionSubtitle"
|
||||
value="<%= data.pricingSection?.subtitle || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input type="text" class="form-control" id="pricingSectionHeading"
|
||||
value="<%= data.pricingSection?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" id="pricingSectionDescription"
|
||||
rows="3"><%= data.pricingSection?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plans Tab -->
|
||||
<div class="tab-pane fade" id="plans" role="tabpanel">
|
||||
<!-- Monthly Plans -->
|
||||
<div class="card border shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-medium mb-0">Monthly Plans</h6>
|
||||
<button type="button" class="btn btn-primary btn-sm"
|
||||
onclick="addPlan('monthly')">
|
||||
<i class="fas fa-plus"></i> Add Plan
|
||||
</button>
|
||||
</div>
|
||||
<div id="monthlyPlansContainer">
|
||||
<% if (data.plans?.monthly && data.plans.monthly.length> 0) { %>
|
||||
<% data.plans.monthly.forEach((plan, index)=> { %>
|
||||
<div class="card mb-3 plan-item" data-type="monthly">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Plan Name</label>
|
||||
<input type="text" class="form-control plan-name"
|
||||
value="<%= plan.name || '' %>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Price</label>
|
||||
<input type="text" class="form-control plan-price"
|
||||
value="<%= plan.price || '' %>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Currency</label>
|
||||
<input type="text"
|
||||
class="form-control plan-currency"
|
||||
value="<%= plan.currency || '$' %>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Period</label>
|
||||
<input type="text" class="form-control plan-period"
|
||||
value="<%= plan.period || 'mo' %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Style</label>
|
||||
<select class="form-select plan-style">
|
||||
<option value="default"
|
||||
<%=plan.style==='default' ? 'selected' : ''
|
||||
%>>Default</option>
|
||||
<option value="style-2"
|
||||
<%=plan.style==='style-2' ? 'selected' : ''
|
||||
%>>Style 2 (Featured)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Button Text</label>
|
||||
<input type="text"
|
||||
class="form-control plan-button-text"
|
||||
value="<%= plan.buttonText || 'Get Started Today' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Button Link</label>
|
||||
<input type="text"
|
||||
class="form-control plan-button-link"
|
||||
value="<%= plan.buttonLink || '/pricing' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Button Icon</label>
|
||||
<input type="text"
|
||||
class="form-control plan-button-icon"
|
||||
value="<%= plan.buttonIcon || 'fa-solid fa-arrow-right' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Features (one per
|
||||
line)</label>
|
||||
<textarea class="form-control plan-features"
|
||||
rows="4"><%= (plan.features || []).join('\n') %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger btn-sm mt-3"
|
||||
onclick="removePlan(this)">
|
||||
<i class="fas fa-trash me-2"></i>Remove Plan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Yearly Plans -->
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-medium mb-0">Yearly Plans</h6>
|
||||
<button type="button" class="btn btn-primary btn-sm"
|
||||
onclick="addPlan('yearly')">
|
||||
<i class="fas fa-plus"></i> Add Plan
|
||||
</button>
|
||||
</div>
|
||||
<div id="yearlyPlansContainer">
|
||||
<% if (data.plans?.yearly && data.plans.yearly.length> 0) { %>
|
||||
<% data.plans.yearly.forEach((plan, index)=> { %>
|
||||
<div class="card mb-3 plan-item" data-type="yearly">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Plan Name</label>
|
||||
<input type="text" class="form-control plan-name"
|
||||
value="<%= plan.name || '' %>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Price</label>
|
||||
<input type="text" class="form-control plan-price"
|
||||
value="<%= plan.price || '' %>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Currency</label>
|
||||
<input type="text"
|
||||
class="form-control plan-currency"
|
||||
value="<%= plan.currency || '$' %>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Period</label>
|
||||
<input type="text" class="form-control plan-period"
|
||||
value="<%= plan.period || 'mo' %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Style</label>
|
||||
<select class="form-select plan-style">
|
||||
<option value="default"
|
||||
<%=plan.style==='default' ? 'selected' : ''
|
||||
%>>Default</option>
|
||||
<option value="style-2"
|
||||
<%=plan.style==='style-2' ? 'selected' : ''
|
||||
%>>Style 2 (Featured)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Button Text</label>
|
||||
<input type="text"
|
||||
class="form-control plan-button-text"
|
||||
value="<%= plan.buttonText || 'Get Started Today' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Button Link</label>
|
||||
<input type="text"
|
||||
class="form-control plan-button-link"
|
||||
value="<%= plan.buttonLink || '/pricing' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Button Icon</label>
|
||||
<input type="text"
|
||||
class="form-control plan-button-icon"
|
||||
value="<%= plan.buttonIcon || 'fa-solid fa-arrow-right' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Features (one per
|
||||
line)</label>
|
||||
<textarea class="form-control plan-features"
|
||||
rows="4"><%= (plan.features || []).join('\n') %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger btn-sm mt-3"
|
||||
onclick="removePlan(this)">
|
||||
<i class="fas fa-trash me-2"></i>Remove Plan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Testimonials Tab -->
|
||||
<div class="tab-pane fade" id="testimonials" role="tabpanel">
|
||||
<div class="card border shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="fw-medium mb-3">Testimonials Section Header</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subtitle</label>
|
||||
<input type="text" class="form-control" id="testimonialsSubtitle"
|
||||
value="<%= data.testimonials?.subtitle || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input type="text" class="form-control" id="testimonialsHeading"
|
||||
value="<%= data.testimonials?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-medium">Button Text</label>
|
||||
<input type="text" class="form-control" id="testimonialsButtonText"
|
||||
value="<%= data.testimonials?.buttonText || '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-medium">Button Link</label>
|
||||
<input type="text" class="form-control" id="testimonialsButtonLink"
|
||||
value="<%= data.testimonials?.buttonLink || '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-medium">Section Image</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="testimonialsImage"
|
||||
value="<%= data.testimonials?.image || '' %>">
|
||||
<button type="button"
|
||||
class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="testimonialsImage" data-image-type="pricing">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-medium mb-0">Testimonial Items</h6>
|
||||
<button type="button" class="btn btn-primary btn-sm"
|
||||
onclick="addTestimonial()">
|
||||
<i class="fas fa-plus"></i> Add Testimonial
|
||||
</button>
|
||||
</div>
|
||||
<div id="testimonialsContainer">
|
||||
<% if (data.testimonials?.items && data.testimonials.items.length> 0) { %>
|
||||
<% data.testimonials.items.forEach((item, index)=> { %>
|
||||
<div class="card mb-3 testimonial-item">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text"
|
||||
class="form-control testimonial-name"
|
||||
value="<%= item.name || '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Role/Type</label>
|
||||
<input type="text"
|
||||
class="form-control testimonial-role"
|
||||
value="<%= item.role || '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Rating</label>
|
||||
<select class="form-select testimonial-rating">
|
||||
<option value="1" <%=item.rating===1
|
||||
? 'selected' : '' %>>1 Star</option>
|
||||
<option value="2" <%=item.rating===2
|
||||
? 'selected' : '' %>>2 Stars</option>
|
||||
<option value="3" <%=item.rating===3
|
||||
? 'selected' : '' %>>3 Stars</option>
|
||||
<option value="4" <%=item.rating===4
|
||||
? 'selected' : '' %>>4 Stars</option>
|
||||
<option value="5" <%=item.rating===5
|
||||
? 'selected' : '' %>>5 Stars</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Content</label>
|
||||
<textarea class="form-control testimonial-content"
|
||||
rows="3"><%= item.content || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger btn-sm mt-3"
|
||||
onclick="removeTestimonial(this)">
|
||||
<i class="fas fa-trash me-2"></i>Remove Testimonial
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fixed Bottom Buttons -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
|
||||
<i class="fas fa-undo me-2"></i>Reset
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<i class="fas fa-save me-2"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="application/json" id="pricingDataJson"><%- JSON.stringify(data) %></script>
|
||||
|
||||
<script>
|
||||
let originalFormData = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
try {
|
||||
var jsonScript = document.getElementById('pricingDataJson');
|
||||
originalFormData = JSON.parse(jsonScript.textContent);
|
||||
} catch (e) {
|
||||
console.error('Error parsing originalFormData:', e);
|
||||
originalFormData = {};
|
||||
}
|
||||
updateAllJsonInputs();
|
||||
initializeFormHandlers();
|
||||
});
|
||||
|
||||
function initializeFormHandlers() {
|
||||
const form = document.getElementById('pricingForm');
|
||||
form.addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
|
||||
|
||||
try {
|
||||
updateJsonData();
|
||||
this.submit();
|
||||
} catch (error) {
|
||||
console.error('Error updating data:', error);
|
||||
alert('Failed to process form data. Please try again.');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
|
||||
}
|
||||
});
|
||||
|
||||
// Image upload buttons
|
||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
const imageType = this.dataset.imageType;
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
});
|
||||
|
||||
// Update preview when background image changes
|
||||
document.getElementById('heroBackgroundImage').addEventListener('input', function () {
|
||||
updateHeroImagePreview(this.value);
|
||||
});
|
||||
}
|
||||
|
||||
function updateHeroImagePreview(imagePath) {
|
||||
const previewContainer = document.getElementById('heroImagePreview');
|
||||
if (imagePath) {
|
||||
let imgSrc = imagePath;
|
||||
if (!imgSrc.startsWith('http://') && !imgSrc.startsWith('https://')) {
|
||||
imgSrc = imgSrc.startsWith('/') ? imgSrc : '/' + imgSrc;
|
||||
}
|
||||
previewContainer.innerHTML = `
|
||||
<img src="${imgSrc}" class="img-thumbnail" id="heroPreviewImg"
|
||||
style="height: 200px; width: 100%; object-fit: cover;"
|
||||
alt="Background image preview"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
||||
<div class="border rounded p-5 text-center text-muted"
|
||||
style="height: 200px; display: none; align-items: center; justify-content: center;">
|
||||
Image preview
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
previewContainer.innerHTML = `
|
||||
<div class="border rounded p-5 text-center text-muted"
|
||||
style="height: 200px; display: flex; align-items: center; justify-content: center;">
|
||||
Image preview
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateAllJsonInputs() {
|
||||
updateJsonData();
|
||||
}
|
||||
|
||||
function updateJsonData() {
|
||||
// Hero data
|
||||
const heroData = {
|
||||
title: document.getElementById('heroTitle').value || '',
|
||||
backgroundImage: document.getElementById('heroBackgroundImage').value || '',
|
||||
shapeImage: originalFormData?.hero?.shapeImage || '/assets/img/inner-page/shape.png',
|
||||
breadcrumb: originalFormData?.hero?.breadcrumb || [],
|
||||
};
|
||||
document.getElementById('heroJson').value = JSON.stringify(heroData);
|
||||
|
||||
// Pricing Section data
|
||||
const pricingSectionData = {
|
||||
subtitle: document.getElementById('pricingSectionSubtitle').value || '',
|
||||
heading: document.getElementById('pricingSectionHeading').value || '',
|
||||
description: document.getElementById('pricingSectionDescription').value || '',
|
||||
};
|
||||
document.getElementById('pricingSectionJson').value = JSON.stringify(pricingSectionData);
|
||||
|
||||
// Plans data
|
||||
const monthlyPlans = [];
|
||||
document.querySelectorAll('#monthlyPlansContainer .plan-item').forEach(item => {
|
||||
const featuresText = item.querySelector('.plan-features').value || '';
|
||||
monthlyPlans.push({
|
||||
name: item.querySelector('.plan-name').value || '',
|
||||
price: item.querySelector('.plan-price').value || '0',
|
||||
currency: item.querySelector('.plan-currency').value || '$',
|
||||
period: item.querySelector('.plan-period').value || 'mo',
|
||||
style: item.querySelector('.plan-style').value || 'default',
|
||||
buttonText: item.querySelector('.plan-button-text').value || 'Get Started Today',
|
||||
buttonLink: item.querySelector('.plan-button-link').value || '/pricing',
|
||||
buttonIcon: item.querySelector('.plan-button-icon').value || 'fa-solid fa-arrow-right',
|
||||
features: featuresText.split('\n').filter(f => f.trim()),
|
||||
});
|
||||
});
|
||||
|
||||
const yearlyPlans = [];
|
||||
document.querySelectorAll('#yearlyPlansContainer .plan-item').forEach(item => {
|
||||
const featuresText = item.querySelector('.plan-features').value || '';
|
||||
yearlyPlans.push({
|
||||
name: item.querySelector('.plan-name').value || '',
|
||||
price: item.querySelector('.plan-price').value || '0',
|
||||
currency: item.querySelector('.plan-currency').value || '$',
|
||||
period: item.querySelector('.plan-period').value || 'mo',
|
||||
style: item.querySelector('.plan-style').value || 'default',
|
||||
buttonText: item.querySelector('.plan-button-text').value || 'Get Started Today',
|
||||
buttonLink: item.querySelector('.plan-button-link').value || '/pricing',
|
||||
buttonIcon: item.querySelector('.plan-button-icon').value || 'fa-solid fa-arrow-right',
|
||||
features: featuresText.split('\n').filter(f => f.trim()),
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('plansJson').value = JSON.stringify({
|
||||
monthly: monthlyPlans,
|
||||
yearly: yearlyPlans,
|
||||
});
|
||||
|
||||
// Testimonials data
|
||||
const testimonialItems = [];
|
||||
document.querySelectorAll('#testimonialsContainer .testimonial-item').forEach(item => {
|
||||
testimonialItems.push({
|
||||
name: item.querySelector('.testimonial-name').value || '',
|
||||
role: item.querySelector('.testimonial-role').value || '',
|
||||
rating: parseInt(item.querySelector('.testimonial-rating').value) || 5,
|
||||
content: item.querySelector('.testimonial-content').value || '',
|
||||
});
|
||||
});
|
||||
|
||||
const testimonialsData = {
|
||||
subtitle: document.getElementById('testimonialsSubtitle').value || '',
|
||||
heading: document.getElementById('testimonialsHeading').value || '',
|
||||
buttonText: document.getElementById('testimonialsButtonText').value || '',
|
||||
buttonLink: document.getElementById('testimonialsButtonLink').value || '',
|
||||
buttonIcon: originalFormData?.testimonials?.buttonIcon || 'fa-solid fa-arrow-right',
|
||||
image: document.getElementById('testimonialsImage').value || '',
|
||||
items: testimonialItems,
|
||||
};
|
||||
document.getElementById('testimonialsJson').value = JSON.stringify(testimonialsData);
|
||||
}
|
||||
|
||||
function addPlan(type) {
|
||||
const container = document.getElementById(type + 'PlansContainer');
|
||||
const html = `
|
||||
<div class="card mb-3 plan-item" data-type="${type}">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Plan Name</label>
|
||||
<input type="text" class="form-control plan-name" value="">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Price</label>
|
||||
<input type="text" class="form-control plan-price" value="">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Currency</label>
|
||||
<input type="text" class="form-control plan-currency" value="$">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Period</label>
|
||||
<input type="text" class="form-control plan-period" value="mo">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Style</label>
|
||||
<select class="form-select plan-style">
|
||||
<option value="default" selected>Default</option>
|
||||
<option value="style-2">Style 2 (Featured)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Button Text</label>
|
||||
<input type="text" class="form-control plan-button-text" value="Get Started Today">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Button Link</label>
|
||||
<input type="text" class="form-control plan-button-link" value="/pricing">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Button Icon</label>
|
||||
<input type="text" class="form-control plan-button-icon" value="fa-solid fa-arrow-right">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Features (one per line)</label>
|
||||
<textarea class="form-control plan-features" rows="4"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removePlan(this)">
|
||||
<i class="fas fa-trash me-2"></i>Remove Plan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function removePlan(button) {
|
||||
if (confirm('Are you sure you want to remove this plan?')) {
|
||||
button.closest('.plan-item').remove();
|
||||
}
|
||||
}
|
||||
|
||||
function addTestimonial() {
|
||||
const container = document.getElementById('testimonialsContainer');
|
||||
const html = `
|
||||
<div class="card mb-3 testimonial-item">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control testimonial-name" value="">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Role/Type</label>
|
||||
<input type="text" class="form-control testimonial-role" value="">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Rating</label>
|
||||
<select class="form-select testimonial-rating">
|
||||
<option value="1">1 Star</option>
|
||||
<option value="2">2 Stars</option>
|
||||
<option value="3">3 Stars</option>
|
||||
<option value="4">4 Stars</option>
|
||||
<option value="5" selected>5 Stars</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Content</label>
|
||||
<textarea class="form-control testimonial-content" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeTestimonial(this)">
|
||||
<i class="fas fa-trash me-2"></i>Remove Testimonial
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function removeTestimonial(button) {
|
||||
if (confirm('Are you sure you want to remove this testimonial?')) {
|
||||
button.closest('.testimonial-item').remove();
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
if (confirm('Are you sure you want to reset all changes?')) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
// Image uploader function
|
||||
function openImageUploader(targetInput, imageType) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
try {
|
||||
// Send imageType via query string as controller expects req.query.imageType
|
||||
const uploadUrl = '/admin/upload/image?imageType=' + encodeURIComponent(imageType || 'general');
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success && result.path) {
|
||||
document.getElementById(targetInput).value = result.path;
|
||||
if (targetInput === 'heroBackgroundImage') {
|
||||
updateHeroImagePreview(result.path);
|
||||
}
|
||||
} else {
|
||||
alert('Upload failed: ' + (result.error || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
alert('Upload failed. Please try again.');
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
</script>
|
||||
124
views/admin/qualification/create.ejs
Normal file
124
views/admin/qualification/create.ejs
Normal file
@@ -0,0 +1,124 @@
|
||||
<div class="page-title-area">
|
||||
<div>
|
||||
<h1>Create Qualification</h1>
|
||||
<p class="subtitle">Register a new degree / qualification record</p>
|
||||
</div>
|
||||
<a href="/admin/qualification" class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> Back</a>
|
||||
</div>
|
||||
|
||||
<% if (typeof error !== 'undefined' && error) { %>
|
||||
<div class="alert d-flex align-items-center gap-2 mb-3" style="background:var(--danger-soft);color:var(--danger-color);border:none;border-radius:var(--border-radius-sm);">
|
||||
<i class="fas fa-exclamation-circle"></i> <%= error %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/admin/qualification/create" enctype="multipart/form-data">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-8">
|
||||
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-id-card"></i> Basic Information</h5></div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Qualification No. <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="qualification_number" required
|
||||
value="<%= (typeof formData !== 'undefined' && formData) ? formData.qualification_number || '' : '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Student Full Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="student_name" required
|
||||
value="<%= (typeof formData !== 'undefined' && formData) ? formData.student_name || '' : '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Program Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="program_name" required
|
||||
value="<%= (typeof formData !== 'undefined' && formData) ? formData.program_name || '' : '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Passport No.</label>
|
||||
<input type="text" class="form-control" name="passport_number"
|
||||
value="<%= (typeof formData !== 'undefined' && formData) ? formData.passport_number || '' : '' %>">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Address</label>
|
||||
<input type="text" class="form-control" name="address"
|
||||
value="<%= (typeof formData !== 'undefined' && formData) ? formData.address || '' : '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-tags"></i> Classification</h5></div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Department <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="department" required>
|
||||
<option value="">-- Select --</option>
|
||||
<% departments.forEach(d => { %>
|
||||
<option value="<%= d._id %>" <%= (typeof formData !== 'undefined' && formData && formData.department === d._id.toString()) ? 'selected' : '' %>><%= d.name %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Level <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="level" required>
|
||||
<option value="">-- Select --</option>
|
||||
<% levels.forEach(l => { %>
|
||||
<option value="<%= l._id %>" <%= (typeof formData !== 'undefined' && formData && formData.level === l._id.toString()) ? 'selected' : '' %>><%= l.type %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Issue Date <span class="text-danger">*</span></label>
|
||||
<input type="date" class="form-control" name="issued_date" required
|
||||
value="<%= (typeof formData !== 'undefined' && formData && formData.issued_date) ? formData.issued_date.toString().substring(0,10) : '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="active">Active</option>
|
||||
<option value="revoked" <%= (typeof formData !== 'undefined' && formData && formData.status === 'revoked') ? 'selected' : '' %>>Revoked</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-book-open"></i> Thesis / Topic <span style="font-size:0.75rem;color:var(--text-muted);font-weight:400">(fill for PhD — leave empty for MBA/Master)</span></h5></div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Topic Title</label>
|
||||
<input type="text" class="form-control" name="topic_name"
|
||||
value="<%= (typeof formData !== 'undefined' && formData) ? formData.topic_name || '' : '' %>">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Short Description</label>
|
||||
<textarea class="form-control" name="topic_short_desc" rows="3"><%= (typeof formData !== 'undefined' && formData) ? formData.topic_short_desc || '' : '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-image"></i> Degree Image</h5></div>
|
||||
<div class="card-body">
|
||||
<input type="file" class="form-control" name="degree_image" accept="image/*">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card border-0">
|
||||
<div class="card-body d-flex flex-column gap-2">
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="fas fa-save"></i> Create Qualification</button>
|
||||
<a href="/admin/qualification" class="btn btn-outline-secondary w-100">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
123
views/admin/qualification/edit.ejs
Normal file
123
views/admin/qualification/edit.ejs
Normal file
@@ -0,0 +1,123 @@
|
||||
<div class="page-title-area">
|
||||
<div>
|
||||
<h1>Edit Qualification</h1>
|
||||
<p class="subtitle"><code style="font-size:0.8rem;color:var(--accent-color)"><%= qual.qualification_number %></code></p>
|
||||
</div>
|
||||
<a href="/admin/qualification" class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> Back</a>
|
||||
</div>
|
||||
|
||||
<% if (typeof error !== 'undefined' && error) { %>
|
||||
<div class="alert d-flex align-items-center gap-2 mb-3" style="background:var(--danger-soft);color:var(--danger-color);border:none;border-radius:var(--border-radius-sm);">
|
||||
<i class="fas fa-exclamation-circle"></i> <%= error %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/admin/qualification/<%= qual._id %>/edit" enctype="multipart/form-data">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-8">
|
||||
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-id-card"></i> Basic Information</h5></div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Qualification No. <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="qualification_number" required value="<%= qual.qualification_number %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Student Full Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="student_name" required value="<%= qual.student_name %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Program Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="program_name" required value="<%= qual.program_name %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Passport No.</label>
|
||||
<input type="text" class="form-control" name="passport_number" value="<%= qual.passport_number || '' %>">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Address</label>
|
||||
<input type="text" class="form-control" name="address" value="<%= qual.address || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-tags"></i> Classification</h5></div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Department <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="department" required>
|
||||
<option value="">-- Select --</option>
|
||||
<% departments.forEach(d => { %>
|
||||
<option value="<%= d._id %>" <%= (qual.department && qual.department._id && qual.department._id.toString() === d._id.toString()) ? 'selected' : '' %>><%= d.name %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Level <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="level" required>
|
||||
<option value="">-- Select --</option>
|
||||
<% levels.forEach(l => { %>
|
||||
<option value="<%= l._id %>" <%= (qual.level && qual.level._id && qual.level._id.toString() === l._id.toString()) ? 'selected' : '' %>><%= l.type %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Issue Date <span class="text-danger">*</span></label>
|
||||
<input type="date" class="form-control" name="issued_date" required value="<%= qual.issued_date ? new Date(qual.issued_date).toISOString().substring(0,10) : '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="active" <%= qual.status === 'active' ? 'selected' : '' %>>Active</option>
|
||||
<option value="revoked" <%= qual.status === 'revoked' ? 'selected' : '' %>>Revoked</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-book-open"></i> Thesis / Topic</h5></div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Topic Title</label>
|
||||
<input type="text" class="form-control" name="topic_name" value="<%= qual.topic_name || '' %>">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Short Description</label>
|
||||
<textarea class="form-control" name="topic_short_desc" rows="3"><%= qual.topic_short_desc || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-image"></i> Degree Image</h5></div>
|
||||
<div class="card-body">
|
||||
<% if (qual.degree_image) { %>
|
||||
<div class="mb-2">
|
||||
<img src="/admin/files/<%= qual.degree_image %>" alt="Degree image" class="img-thumbnail" style="max-height:110px;border-radius:var(--border-radius-sm);">
|
||||
</div>
|
||||
<% } %>
|
||||
<input type="file" class="form-control" name="degree_image" accept="image/*">
|
||||
<div class="form-text">Leave empty to keep current image.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card border-0">
|
||||
<div class="card-body d-flex flex-column gap-2">
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="fas fa-save"></i> Save Changes</button>
|
||||
<a href="/admin/qualification" class="btn btn-outline-secondary w-100">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
110
views/admin/qualification/index.ejs
Normal file
110
views/admin/qualification/index.ejs
Normal file
@@ -0,0 +1,110 @@
|
||||
<div class="page-title-area">
|
||||
<div>
|
||||
<h1>Qualifications</h1>
|
||||
<p class="subtitle">Degree / qualification records</p>
|
||||
</div>
|
||||
<a href="/admin/qualification/create" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> New Qualification
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Filter -->
|
||||
<div class="card border-0 mb-3">
|
||||
<div class="card-body" style="padding:1rem 1.25rem;">
|
||||
<form method="GET" action="/admin/qualification">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<div class="position-relative search-bar">
|
||||
<i class="fas fa-search search-icon"></i>
|
||||
<input type="text" class="form-control" name="search" placeholder="Search name, number..." value="<%= query.search || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" name="status">
|
||||
<option value="">All Status</option>
|
||||
<option value="active" <%= query.status === 'active' ? 'selected' : '' %>>Active</option>
|
||||
<option value="revoked" <%= query.status === 'revoked' ? 'selected' : '' %>>Revoked</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1 d-flex gap-1">
|
||||
<button type="submit" class="btn btn-primary flex-fill"><i class="fas fa-search"></i></button>
|
||||
<% if (query.search || query.status) { %>
|
||||
<a href="/admin/qualification" class="btn btn-outline-secondary"><i class="fas fa-times"></i></a>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card border-0">
|
||||
<div class="card-body p-0">
|
||||
<% if (qualifications && qualifications.length > 0) { %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px">#</th>
|
||||
<th>Qualification No.</th>
|
||||
<th>Full Name</th>
|
||||
<th>Program</th>
|
||||
<th>Department</th>
|
||||
<th>Level</th>
|
||||
<th>Issue Date</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th style="width:90px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% qualifications.forEach((q, i) => { %>
|
||||
<tr>
|
||||
<td style="color:var(--text-muted)"><%= i + 1 %></td>
|
||||
<td><code style="font-size:0.8rem;color:var(--primary-color)"><%= q.qualification_number %></code></td>
|
||||
<td style="font-weight:500"><%= q.student_name %></td>
|
||||
<td style="font-size:0.8125rem;color:var(--text-muted)"><%= q.program_name %></td>
|
||||
<td style="font-size:0.8125rem"><%= q.department ? q.department.name : '—' %></td>
|
||||
<td style="font-size:0.8125rem"><%= q.level ? q.level.type : '—' %></td>
|
||||
<td style="font-size:0.8125rem;color:var(--text-muted)"><%= q.issued_date ? new Date(q.issued_date).toLocaleDateString('en-GB') : '—' %></td>
|
||||
<td>
|
||||
<% if (q.topic_name) { %>
|
||||
<span class="badge badge-soft-primary">PhD</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-soft-info">MBA/Master</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (q.status === 'active') { %>
|
||||
<span class="badge bg-soft-success">Active</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-soft-danger">Revoked</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-actions">
|
||||
<a href="/admin/qualification/<%= q._id %>/edit" class="btn btn-sm btn-outline-primary btn-icon" title="Edit">
|
||||
<i class="fas fa-pen"></i>
|
||||
</a>
|
||||
<form method="POST" action="/admin/qualification/<%= q._id %>/delete" style="display:inline;" onsubmit="return confirm('Delete this qualification?')">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger btn-icon" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon"><i class="fas fa-graduation-cap"></i></div>
|
||||
<h5>No qualifications found</h5>
|
||||
<p>Create the first qualification record.</p>
|
||||
<a href="/admin/qualification/create" class="btn btn-primary"><i class="fas fa-plus"></i> Create</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,755 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">Service Details: <%= service.name %></h1>
|
||||
<p class="text-muted mb-0">Edit detailed content for <%= service.name %> service</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/service" class="btn btn-outline-primary">
|
||||
<i class="fas fa-arrow-left me-2"></i>Back to Services
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form action="/admin/service/<%= service.slug %>/details/update" method="POST" class="content-with-fixed-buttons" id="serviceDetailsForm">
|
||||
<!-- Hidden inputs for JSON data -->
|
||||
<input type="hidden" name="details" id="detailsJson">
|
||||
<input type="hidden" name="features" id="featuresJson">
|
||||
<input type="hidden" name="faq" id="faqJson">
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#basic-info" role="tab">
|
||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#key-features" role="tab">
|
||||
<i class="fas fa-star me-2"></i>Key Features
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#faq-section" role="tab">
|
||||
<i class="fas fa-question-circle me-2"></i>FAQ Section
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<!-- Basic Information Tab -->
|
||||
<div class="tab-pane fade show active" id="basic-info" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label fw-medium">Main Image</label>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="mainImage" name="mainImage"
|
||||
value="<%= service.details?.mainImage || '' %>">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="mainImage" data-image-type="service">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text text-muted">Recommended size: 800x600px</small>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div id="mainImagePreview">
|
||||
<% if (service.details?.mainImage) { %>
|
||||
<img src="<%= getFullImageUrl(service.details.mainImage) %>" class="img-thumbnail" style="max-height: 250px; width: auto; max-width: 100%; object-fit: contain;" alt="Main image preview">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="detailsTitle" name="title"
|
||||
value="<%= service.details?.title || service.name %>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" id="detailsDescription" name="description" rows="3"><%= service.details?.description || service.description %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overview Section -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card border">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0">Overview Section</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Overview Title</label>
|
||||
<input type="text" class="form-control" id="overviewTitle" name="overviewTitle"
|
||||
value="<%= service.details?.overviewTitle || 'Service Overview' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Overview Description</label>
|
||||
<textarea class="form-control" id="overviewDescription" name="overviewDescription" rows="4"><%= service.details?.overviewDescription || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Additional Description</label>
|
||||
<textarea class="form-control" id="additionalDescription" name="additionalDescription" rows="3"><%= service.details?.additionalDescription || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Features Tab -->
|
||||
<div class="tab-pane fade" id="key-features" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-body">
|
||||
<!-- Features Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Features Title</label>
|
||||
<input type="text" class="form-control" id="keyFeaturesTitle" name="keyFeaturesTitle"
|
||||
value="<%= service.details?.keyFeaturesTitle || 'Key Features' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Features Image</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="keyFeaturesImage" name="keyFeaturesImage"
|
||||
value="<%= service.details?.keyFeaturesImage || '' %>">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="keyFeaturesImage" data-image-type="service">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Features Image Preview -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<div id="keyFeaturesImagePreview">
|
||||
<% if (service.details?.keyFeaturesImage) { %>
|
||||
<img src="<%= getFullImageUrl(service.details.keyFeaturesImage) %>" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="Key Features image preview">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features List -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-medium mb-0">Features</h6>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="addFeature()">
|
||||
<i class="fas fa-plus"></i> Add Feature
|
||||
</button>
|
||||
</div>
|
||||
<div id="featuresContainer">
|
||||
<% if (service.details?.features && service.details.features.length > 0) { %>
|
||||
<% service.details.features.forEach((feature, index) => { %>
|
||||
<div class="card mb-3 feature-item">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="mb-0 text-decoration-underline">Feature <%= index + 1 %></h6>
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeFeature(this)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control feature-title"
|
||||
value="<%= feature.title || '' %>" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control feature-description" rows="2" required><%= feature.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAQ Section Tab -->
|
||||
<div class="tab-pane fade" id="faq-section" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-body">
|
||||
<!-- FAQ Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">FAQ Title</label>
|
||||
<input type="text" class="form-control" id="faqTitle" name="faqTitle"
|
||||
value="<%= service.details?.faqTitle || 'Frequently Asked Questions' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">FAQ Image</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="faqImage" name="faqImage"
|
||||
value="<%= service.details?.faqImage || '' %>">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="faqImage" data-image-type="service">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAQ Image Preview -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<div id="faqImagePreview">
|
||||
<% if (service.details?.faqImage) { %>
|
||||
<img src="<%= getFullImageUrl(service.details.faqImage) %>" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="FAQ image preview">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAQ List -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-medium mb-0">FAQ Items</h6>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="addFAQ()">
|
||||
<i class="fas fa-plus"></i> Add FAQ
|
||||
</button>
|
||||
</div>
|
||||
<div id="faqContainer">
|
||||
<% if (service.details?.faq && service.details.faq.length > 0) { %>
|
||||
<% service.details.faq.forEach((faq, index) => { %>
|
||||
<div class="card mb-3 faq-item">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="mb-0 text-decoration-underline">FAQ <%= index + 1 %></h6>
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeFAQ(this)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">ID (Auto-generated)</label>
|
||||
<input type="text" class="form-control faq-id"
|
||||
value="<%= faq.id || 'faq-' + (index + 1) %>" readonly>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Expanded by Default</label>
|
||||
<select class="form-control faq-expanded">
|
||||
<option value="false" <%= !faq.isExpanded ? 'selected' : '' %>>No</option>
|
||||
<option value="true" <%= faq.isExpanded ? 'selected' : '' %>>Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Question</label>
|
||||
<input type="text" class="form-control faq-question"
|
||||
value="<%= faq.question || '' %>" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Answer</label>
|
||||
<textarea class="form-control faq-answer" rows="3" required><%= faq.answer || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Buttons -->
|
||||
<div class="bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary">
|
||||
<i class="fas fa-undo me-2"></i>Reset
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<i class="fas fa-save me-2"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Scripts -->
|
||||
<script>
|
||||
let originalFormData = null;
|
||||
let featureIndex = <%= service.details?.features?.length || 0 %>;
|
||||
let faqIndex = <%= service.details?.faq?.length || 0 %>;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize form data
|
||||
originalFormData = <%- JSON.stringify(service) %>;
|
||||
|
||||
// Set initial JSON values
|
||||
updateAllJsonInputs(originalFormData);
|
||||
|
||||
// Initialize form handlers
|
||||
initializeFormHandlers();
|
||||
});
|
||||
|
||||
function initializeFormHandlers() {
|
||||
// Form submission
|
||||
const form = document.getElementById('serviceDetailsForm');
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
|
||||
|
||||
try {
|
||||
updateJsonData();
|
||||
this.submit();
|
||||
} catch (error) {
|
||||
console.error('Error updating data:', error);
|
||||
showError('Failed to process form data. Please try again.');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize image upload buttons
|
||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
const imageType = this.dataset.imageType;
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize image input change listeners for manual URL input
|
||||
const imageInputs = ['mainImage', 'keyFeaturesImage', 'faqImage'];
|
||||
imageInputs.forEach(inputId => {
|
||||
const input = document.getElementById(inputId);
|
||||
if (input) {
|
||||
input.addEventListener('input', function() {
|
||||
updateImagePreviewAfterUpload(inputId, this.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addFeature() {
|
||||
const container = document.getElementById('featuresContainer');
|
||||
const featureHtml = `
|
||||
<div class="card mb-3 feature-item">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="mb-0 text-decoration-underline">Feature ${featureIndex + 1}</h6>
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeFeature(this)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control feature-title" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control feature-description" rows="2" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', featureHtml);
|
||||
featureIndex++;
|
||||
}
|
||||
|
||||
function removeFeature(button) {
|
||||
const featureItem = button.closest('.feature-item');
|
||||
if (featureItem) {
|
||||
featureItem.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function addFAQ() {
|
||||
const container = document.getElementById('faqContainer');
|
||||
const newFaqId = generateFAQId();
|
||||
const faqNumber = document.querySelectorAll('.faq-item').length + 1;
|
||||
const faqHtml = `
|
||||
<div class="card mb-3 faq-item">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="mb-0 text-decoration-underline">FAQ ${faqNumber}</h6>
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeFAQ(this)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">ID (Auto-generated)</label>
|
||||
<input type="text" class="form-control faq-id"
|
||||
value="${newFaqId}" readonly>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Expanded by Default</label>
|
||||
<select class="form-control faq-expanded">
|
||||
<option value="false">No</option>
|
||||
<option value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Question</label>
|
||||
<input type="text" class="form-control faq-question" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Answer</label>
|
||||
<textarea class="form-control faq-answer" rows="3" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', faqHtml);
|
||||
faqIndex++;
|
||||
}
|
||||
|
||||
function updateFAQId(questionInput) {
|
||||
// Không cần update ID nữa vì đã tự động theo số thứ tự
|
||||
}
|
||||
|
||||
function removeFAQ(button) {
|
||||
const faqItem = button.closest('.faq-item');
|
||||
if (faqItem) {
|
||||
faqItem.remove();
|
||||
// Cập nhật lại số thứ tự và ID của tất cả FAQ
|
||||
updateFAQNumbers();
|
||||
}
|
||||
}
|
||||
|
||||
function generateFAQId() {
|
||||
// Đếm số lượng FAQ hiện tại và tạo ID tiếp theo
|
||||
const existingFAQs = document.querySelectorAll('.faq-item');
|
||||
const nextNumber = existingFAQs.length + 1;
|
||||
return `faq-${nextNumber}`;
|
||||
}
|
||||
|
||||
function updateFAQNumbers() {
|
||||
// Cập nhật lại tất cả FAQ ID và số thứ tự
|
||||
const faqItems = document.querySelectorAll('.faq-item');
|
||||
faqItems.forEach((item, index) => {
|
||||
const number = index + 1;
|
||||
const idInput = item.querySelector('.faq-id');
|
||||
const titleElement = item.querySelector('h6');
|
||||
|
||||
if (idInput) {
|
||||
idInput.value = `faq-${number}`;
|
||||
}
|
||||
if (titleElement) {
|
||||
titleElement.textContent = `FAQ ${number}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateAllJsonInputs(data) {
|
||||
// Collect basic details data
|
||||
const details = {
|
||||
title: data.details?.title || data.name,
|
||||
description: data.details?.description || data.description,
|
||||
mainImage: data.details?.mainImage || '',
|
||||
overviewTitle: data.details?.overviewTitle || 'Service Overview',
|
||||
overviewDescription: data.details?.overviewDescription || '',
|
||||
additionalDescription: data.details?.additionalDescription || '',
|
||||
keyFeaturesTitle: data.details?.keyFeaturesTitle || 'Key Features',
|
||||
keyFeaturesImage: data.details?.keyFeaturesImage || '',
|
||||
faqTitle: data.details?.faqTitle || 'Frequently Asked Questions',
|
||||
faqImage: data.details?.faqImage || ''
|
||||
};
|
||||
|
||||
document.getElementById('detailsJson').value = JSON.stringify(details);
|
||||
document.getElementById('featuresJson').value = JSON.stringify(data.details?.features || []);
|
||||
document.getElementById('faqJson').value = JSON.stringify(data.details?.faq || []);
|
||||
}
|
||||
|
||||
function updateJsonData() {
|
||||
// Collect basic details data
|
||||
const details = {
|
||||
title: document.getElementById('detailsTitle').value,
|
||||
description: document.getElementById('detailsDescription').value,
|
||||
mainImage: document.getElementById('mainImage').value,
|
||||
overviewTitle: document.getElementById('overviewTitle').value,
|
||||
overviewDescription: document.getElementById('overviewDescription').value,
|
||||
additionalDescription: document.getElementById('additionalDescription').value,
|
||||
keyFeaturesTitle: document.getElementById('keyFeaturesTitle').value,
|
||||
keyFeaturesImage: document.getElementById('keyFeaturesImage').value,
|
||||
faqTitle: document.getElementById('faqTitle').value,
|
||||
faqImage: document.getElementById('faqImage').value
|
||||
};
|
||||
|
||||
// Collect features data
|
||||
const features = [];
|
||||
document.querySelectorAll('.feature-item').forEach(item => {
|
||||
features.push({
|
||||
title: item.querySelector('.feature-title').value,
|
||||
description: item.querySelector('.feature-description').value
|
||||
});
|
||||
});
|
||||
|
||||
// Collect FAQ data
|
||||
const faq = [];
|
||||
document.querySelectorAll('.faq-item').forEach(item => {
|
||||
faq.push({
|
||||
id: item.querySelector('.faq-id').value,
|
||||
question: item.querySelector('.faq-question').value,
|
||||
answer: item.querySelector('.faq-answer').value,
|
||||
isExpanded: item.querySelector('.faq-expanded').value === 'true'
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('detailsJson').value = JSON.stringify(details);
|
||||
document.getElementById('featuresJson').value = JSON.stringify(features);
|
||||
document.getElementById('faqJson').value = JSON.stringify(faq);
|
||||
}
|
||||
|
||||
// Helper function để tạo full URL cho ảnh - tương tự như server-side helper
|
||||
function getFullImageUrlJS(imagePath) {
|
||||
if (!imagePath) return '';
|
||||
|
||||
// Nếu đã là full URL thì return luôn
|
||||
if (imagePath.startsWith('http')) {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
// Lấy backend URL
|
||||
const backendUrl = '<%= (process.env.BACKEND_URL || "http://localhost:3001").replace(/\/$/, "") %>';
|
||||
|
||||
// Xử lý đường dẫn
|
||||
let imgSrc = imagePath;
|
||||
if (!imgSrc.startsWith('/')) {
|
||||
imgSrc = '/uploads/' + imgSrc;
|
||||
}
|
||||
|
||||
return backendUrl + imgSrc;
|
||||
}
|
||||
|
||||
// Function để cập nhật image preview sau khi upload
|
||||
function updateImagePreviewAfterUpload(targetInput, imagePath) {
|
||||
const fullImageUrl = getFullImageUrlJS(imagePath);
|
||||
|
||||
switch(targetInput) {
|
||||
case 'mainImage':
|
||||
const mainPreview = document.getElementById('mainImagePreview');
|
||||
if (mainPreview) {
|
||||
mainPreview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="max-height: 250px; width: auto; max-width: 100%; object-fit: contain;" alt="Main image preview">`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'keyFeaturesImage':
|
||||
const featuresPreview = document.getElementById('keyFeaturesImagePreview');
|
||||
if (featuresPreview) {
|
||||
featuresPreview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="Key Features image preview">`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'faqImage':
|
||||
const faqPreview = document.getElementById('faqImagePreview');
|
||||
if (faqPreview) {
|
||||
faqPreview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="FAQ image preview">`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function openImageUploader(targetInput, imageType) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.style.display = 'none';
|
||||
document.body.appendChild(fileInput);
|
||||
|
||||
fileInput.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return reject(new Error('No file selected'));
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : null;
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
|
||||
}
|
||||
|
||||
const response = await fetch(`/admin/upload/image?imageType=${encodeURIComponent(imageType)}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Upload failed');
|
||||
}
|
||||
|
||||
const input = document.getElementById(targetInput) || document.querySelector(`[name="${targetInput}"]`);
|
||||
if (!input) throw new Error('Target input not found');
|
||||
|
||||
input.value = result.path;
|
||||
|
||||
// Update image preview based on target input
|
||||
updateImagePreviewAfterUpload(targetInput, result.path);
|
||||
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
} catch (err) {
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
}
|
||||
console.error('Upload error:', err);
|
||||
showError('Upload failed: ' + (err.message || 'Unknown error'));
|
||||
reject(err);
|
||||
} finally {
|
||||
fileInput.remove();
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
// Create and show success alert
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-success alert-dismissible fade show position-fixed';
|
||||
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
// Create and show error alert
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-danger alert-dismissible fade show position-fixed';
|
||||
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.bottom-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
padding: 15px 0;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.content-with-fixed-buttons {
|
||||
/* Remove bottom padding since buttons are no longer fixed */
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.btn-group .btn:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.card-header h6 {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.text-decoration-underline {
|
||||
text-decoration: underline;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
/* Image Preview Styles */
|
||||
#mainImagePreview, #keyFeaturesImagePreview, #faqImagePreview {
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f8f9fa;
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
#mainImagePreview:empty::before,
|
||||
#keyFeaturesImagePreview:empty::before,
|
||||
#faqImagePreview:empty::before {
|
||||
content: "No image selected";
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#mainImagePreview img,
|
||||
#keyFeaturesImagePreview img,
|
||||
#faqImagePreview img {
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
@@ -1,440 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">Edit Service: <%= service.name %></h1>
|
||||
<p class="text-muted mb-0">Update service information and settings</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/service" class="btn btn-outline-primary">
|
||||
<i class="fas fa-arrow-left me-2"></i>Back to Services
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form action="/admin/service/<%= service.slug %>/edit" method="POST" id="editServiceForm">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0" style="color: var(--primary-dark);">
|
||||
<i class="fas fa-edit me-2"></i>Service Information
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Service Name</label>
|
||||
<input type="text" class="form-control" id="serviceName" name="name"
|
||||
value="<%= service.name %>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">
|
||||
Slug
|
||||
<small class="text-muted">(generated from name)</small>
|
||||
<span id="slugAutoIndicator" class="badge bg-info ms-1" style="font-size: 0.7em;">EXISTING</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="serviceSlug" name="slug"
|
||||
value="<%= service.slug %>" readonly>
|
||||
<button type="button" class="btn btn-primary" id="generateSlugBtn" title="Generate slug from name">
|
||||
<i class="fas fa-magic me-1"></i>Generate
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text text-muted">URL-friendly version of the service name.</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Layout</label>
|
||||
<select class="form-control" id="serviceLayout" name="layout">
|
||||
<option value="left" <%= service.layout === 'left' ? 'selected' : '' %>>Left</option>
|
||||
<option value="right" <%= service.layout === 'right' ? 'selected' : '' %>>Right</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label fw-medium">Image</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="serviceImage" name="image"
|
||||
value="<%= service.image || '' %>">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="serviceImage" data-image-type="service">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div id="serviceImagePreview">
|
||||
<% if (service.image) { %>
|
||||
<img src="<%= getFullImageUrl(service.image) %>" class="img-thumbnail"
|
||||
style="height: 80px; width: 100%; object-fit: cover;" alt="Preview">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" id="serviceDescription" name="description"
|
||||
rows="3" required><%= service.description %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Buttons -->
|
||||
<div class="bottom-buttons">
|
||||
<a href="/admin/service" class="btn btn-secondary">
|
||||
<i class="fas fa-times me-2"></i>Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<i class="fas fa-save me-2"></i>Update Service
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script>
|
||||
let servicesData = []; // Will be populated for duplicate checking
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load existing services data for duplicate checking
|
||||
loadServicesData();
|
||||
|
||||
// Initialize form handlers
|
||||
initializeFormHandlers();
|
||||
});
|
||||
|
||||
async function loadServicesData() {
|
||||
try {
|
||||
const response = await fetch('/api/service');
|
||||
const data = await response.json();
|
||||
servicesData = data.services?.items || [];
|
||||
} catch (error) {
|
||||
console.error('Error loading services data:', error);
|
||||
servicesData = [];
|
||||
}
|
||||
}
|
||||
|
||||
function initializeFormHandlers() {
|
||||
// Form submission
|
||||
const form = document.getElementById('editServiceForm');
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Updating...';
|
||||
|
||||
try {
|
||||
// Check for duplicate slug before submitting
|
||||
const slug = document.getElementById('serviceSlug').value.trim();
|
||||
const currentSlug = '<%= service.slug %>';
|
||||
|
||||
if (slug !== currentSlug && isSlugDuplicate(slug, -1)) {
|
||||
showError('Service with this slug already exists. Please generate a new slug.');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Update Service';
|
||||
return;
|
||||
}
|
||||
|
||||
this.submit();
|
||||
} catch (error) {
|
||||
console.error('Error updating service:', error);
|
||||
showError('Failed to update service. Please try again.');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Update Service';
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize image upload buttons
|
||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
const imageType = this.dataset.imageType;
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
});
|
||||
|
||||
// Image preview for service image
|
||||
const serviceImageInput = document.getElementById('serviceImage');
|
||||
if (serviceImageInput) {
|
||||
serviceImageInput.addEventListener('input', function() {
|
||||
updateImagePreview('serviceImagePreview', this.value);
|
||||
});
|
||||
}
|
||||
|
||||
// Generate slug from service name
|
||||
const serviceNameInput = document.getElementById('serviceName');
|
||||
const serviceSlugInput = document.getElementById('serviceSlug');
|
||||
const slugAutoIndicator = document.getElementById('slugAutoIndicator');
|
||||
const generateSlugBtn = document.getElementById('generateSlugBtn');
|
||||
|
||||
if (serviceNameInput && serviceSlugInput && generateSlugBtn) {
|
||||
// Generate slug button
|
||||
generateSlugBtn.addEventListener('click', async function() {
|
||||
const serviceName = serviceNameInput.value.trim();
|
||||
if (serviceName) {
|
||||
// Show loading state
|
||||
const originalBtnHtml = this.innerHTML;
|
||||
this.disabled = true;
|
||||
this.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Generating...';
|
||||
|
||||
try {
|
||||
const slug = await generateSlugFromText(serviceName);
|
||||
const currentSlug = '<%= service.slug %>';
|
||||
|
||||
// Check for duplicate slug (excluding current service)
|
||||
if (slug !== currentSlug && isSlugDuplicate(slug, -1)) {
|
||||
const uniqueSlug = generateUniqueSlug(slug);
|
||||
serviceSlugInput.value = uniqueSlug;
|
||||
showWarning(`Slug "${slug}" already exists. Generated unique slug: "${uniqueSlug}"`);
|
||||
} else {
|
||||
serviceSlugInput.value = slug;
|
||||
showSuccess('Slug generated successfully!');
|
||||
}
|
||||
|
||||
if (slugAutoIndicator) {
|
||||
slugAutoIndicator.textContent = 'GENERATED';
|
||||
slugAutoIndicator.className = 'badge bg-success ms-1';
|
||||
slugAutoIndicator.style.fontSize = '0.7em';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating slug:', error);
|
||||
showError('Failed to generate slug. Please try again.');
|
||||
} finally {
|
||||
// Restore button state
|
||||
this.disabled = false;
|
||||
this.innerHTML = originalBtnHtml;
|
||||
}
|
||||
} else {
|
||||
showError('Please enter a service name first.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Generate slug using backend API
|
||||
async function generateSlugFromText(text) {
|
||||
try {
|
||||
const response = await fetch('/admin/service/generate-slug', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ text: text })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
return result.slug;
|
||||
} else {
|
||||
throw new Error(result.message || 'Failed to generate slug');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating slug:', error);
|
||||
// Fallback to simple slug generation if API fails
|
||||
return text
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^\w\-]+/g, '')
|
||||
.replace(/\-\-+/g, '-')
|
||||
.replace(/^-+/, '')
|
||||
.replace(/-+$/, '');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if slug already exists
|
||||
function isSlugDuplicate(slug, excludeIndex = -1) {
|
||||
return servicesData.some((service, index) => {
|
||||
return service && service.slug === slug && index !== excludeIndex;
|
||||
});
|
||||
}
|
||||
|
||||
// Generate unique slug by appending number
|
||||
function generateUniqueSlug(baseSlug) {
|
||||
let counter = 1;
|
||||
let uniqueSlug = baseSlug;
|
||||
|
||||
while (isSlugDuplicate(uniqueSlug, -1)) {
|
||||
uniqueSlug = baseSlug + '-' + counter;
|
||||
counter++;
|
||||
}
|
||||
|
||||
return uniqueSlug;
|
||||
}
|
||||
|
||||
function updateImagePreview(previewId, imagePath) {
|
||||
const preview = document.getElementById(previewId);
|
||||
if (imagePath) {
|
||||
const fullImageUrl = getFullImageUrlJS(imagePath);
|
||||
preview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="height: 80px; width: 100%; object-fit: cover;" alt="Preview">`;
|
||||
} else {
|
||||
preview.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function để tạo full URL cho ảnh
|
||||
function getFullImageUrlJS(imagePath) {
|
||||
if (!imagePath) return '';
|
||||
|
||||
if (imagePath.startsWith('http')) {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
const backendUrl = '<%= (process.env.BACKEND_URL || "http://localhost:3001").replace(/\/$/, "") %>';
|
||||
|
||||
let imgSrc = imagePath;
|
||||
if (!imgSrc.startsWith('/')) {
|
||||
imgSrc = '/uploads/' + imgSrc;
|
||||
}
|
||||
|
||||
return backendUrl + imgSrc;
|
||||
}
|
||||
|
||||
async function openImageUploader(targetInput, imageType) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.style.display = 'none';
|
||||
document.body.appendChild(fileInput);
|
||||
|
||||
fileInput.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return reject(new Error('No file selected'));
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : null;
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
|
||||
}
|
||||
|
||||
const response = await fetch(`/admin/upload/image?imageType=service`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Upload failed');
|
||||
}
|
||||
|
||||
const input = document.getElementById(targetInput);
|
||||
if (!input) throw new Error('Target input not found');
|
||||
|
||||
input.value = result.path;
|
||||
|
||||
// Update preview
|
||||
if (targetInput === 'serviceImage') {
|
||||
updateImagePreview('serviceImagePreview', result.path);
|
||||
}
|
||||
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
} catch (err) {
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
}
|
||||
console.error('Upload error:', err);
|
||||
showError('Upload failed: ' + (err.message || 'Unknown error'));
|
||||
reject(err);
|
||||
} finally {
|
||||
fileInput.remove();
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-success alert-dismissible fade show position-fixed';
|
||||
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function showWarning(message) {
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-warning alert-dismissible fade show position-fixed';
|
||||
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-danger alert-dismissible fade show position-fixed';
|
||||
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.bottom-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
padding: 15px 0;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.card-header h5 {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,353 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-2" style="color: var(--primary-dark);">
|
||||
Travel Information Editor
|
||||
</h1>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-primary preview-btn">
|
||||
<i class="fas fa-eye me-2"></i>Preview
|
||||
</button>
|
||||
<button type="submit" form="travelForm" class="btn btn-primary" id="saveBtn">
|
||||
<i class="fas fa-save me-2"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="travelForm" action="/admin/travel/update" method="POST" class="needs-validation" novalidate>
|
||||
<input type="hidden" name="hero" id="heroJson">
|
||||
<input type="hidden" name="page" id="pageJson">
|
||||
<input type="hidden" name="content" id="contentJson">
|
||||
<input type="hidden" name="enableScrollspy" id="enableScrollspyInput">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="card-title mb-0">Hero Section</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Background Image</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="heroBackgroundImage"
|
||||
value="<%= data.hero?.backgroundImage || '' %>" readonly>
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="heroBackgroundImage" data-image-type="travel">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text text-muted">Recommended size: 1920x1080px</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div id="heroImagePreview" style="height: 200px; width: 100%;">
|
||||
<% if (data.hero?.backgroundImage) { %>
|
||||
<img src="<%= data.hero.backgroundImage %>" class="img-thumbnail"
|
||||
style="height: 200px; width: 100%; object-fit: cover;" alt="Background image preview">
|
||||
<% } else { %>
|
||||
<div class="border rounded d-flex align-items-center justify-content-center h-100 bg-light">
|
||||
<span class="text-muted">No image selected</span>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Hero Title</label>
|
||||
<textarea class="form-control" id="heroTitle" rows="2"><%= data.hero?.title || 'Travel Information' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="card-title mb-0">Page Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Page Title</label>
|
||||
<input type="text" class="form-control" id="pageTitle" value="<%= data.page?.title || 'Go and Grow Camp Travel Information' %>">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">As of / Year</label>
|
||||
<input type="text" class="form-control" id="pageYear" value="<%= data.page?.year || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="card-title mb-0">Content Editor</h5>
|
||||
<p class="text-muted mb-0 small">Write content using the blog editor</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="editorjs" class="border rounded p-3" style="min-height: 500px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="card-title mb-0">SEO Settings</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Meta Title</label>
|
||||
<input type="text" class="form-control" id="metadataTitle" value="<%= data.page?.metadata?.title || '' %>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Meta Description</label>
|
||||
<textarea class="form-control" id="metadataDescription" rows="3"><%= data.page?.metadata?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="card-title mb-0">Page Settings</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="enableScrollspy" <%= data.enableScrollspy ? 'checked' : '' %>>
|
||||
<label class="form-check-label" for="enableScrollspy">Enable Scrollspy Navigation</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="card-title mb-0">Content Tips</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
<h6><i class="fas fa-lightbulb me-2"></i>Tips for Terms & Conditions:</h6>
|
||||
<ul class="mb-0 small">
|
||||
<li>Use <strong>Header 2</strong> for main sections</li>
|
||||
<li>Use <strong>Header 3</strong> for subsections</li>
|
||||
<li>Use <strong>Lists</strong> for terms items</li>
|
||||
<li>Use <strong>Conclusion</strong> tool for important notes</li>
|
||||
<li>Use <strong>Quote</strong> for legal references</li>
|
||||
</ul>
|
||||
<hr class="my-2">
|
||||
<h6><i class="fas fa-keyboard me-2"></i>Keyboard Shortcuts:</h6>
|
||||
<ul class="mb-0 small">
|
||||
<li><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>H</kbd>: Convert list item to header</li>
|
||||
<li><kbd>Tab</kbd> in list: Indent item</li>
|
||||
<li><kbd>Backspace</kbd> at start: Exit list</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="previewModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Travel Information Preview</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<iframe id="previewFrame" style="width: 100%; height: 600px; border: none;"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="file" id="directImageUpload" style="display: none;" accept="image/*">
|
||||
<input type="hidden" id="currentImageType">
|
||||
<input type="hidden" id="currentTargetInput">
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2.28.2"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@2.7.0"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@2.11.3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/list@1.8.0"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/image@2.8.1"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@2.5.0"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@1.3.0"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@2.5.3"></script>
|
||||
|
||||
<script type="module">
|
||||
import BlogEditor from '/js/editor.js';
|
||||
|
||||
// Logic xử lý lọc dữ liệu để tránh duplicate video và xóa paragraph rỗng
|
||||
class TravelContentManager {
|
||||
cleanEditorData(editorData) {
|
||||
const cleanedBlocks = [];
|
||||
const seenVideoIds = new Set();
|
||||
const youtubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=|embed\/|v\/|shorts\/)?([A-Za-z0-9_-]{11})/;
|
||||
|
||||
(editorData.blocks || []).forEach(block => {
|
||||
if (!block) return;
|
||||
|
||||
// 1. Xử lý Video Embed (Deduplication)
|
||||
if (block.type === 'embed') {
|
||||
const bd = block.data || {};
|
||||
const source = bd.source || bd.embed || '';
|
||||
const match = source.match(youtubeRegex);
|
||||
const vid = bd.videoId || (match ? match[1] : null);
|
||||
|
||||
if (vid) {
|
||||
if (seenVideoIds.has(vid)) return; // Bỏ qua nếu đã có video này
|
||||
seenVideoIds.add(vid);
|
||||
}
|
||||
cleanedBlocks.push(block);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Xử lý Paragraph (Xóa dòng trống hoặc dòng chỉ chứa link đã embed)
|
||||
if (block.type === 'paragraph') {
|
||||
const text = (block.data?.text || '').toString().trim();
|
||||
if (text === '') return; // Xóa paragraph rỗng
|
||||
|
||||
const match = text.match(youtubeRegex);
|
||||
if (match && match[1]) {
|
||||
// Nếu paragraph chỉ chứa link YouTube, và ta sẽ có/đã có block embed cho nó, thì bỏ qua paragraph
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cleanedBlocks.push(block);
|
||||
});
|
||||
|
||||
return { ...editorData, blocks: cleanedBlocks };
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
let blogEditorInstance = null;
|
||||
const travelData = <%- JSON.stringify(data) %>;
|
||||
const initialContent = travelData?.content || { blocks: [] };
|
||||
|
||||
try {
|
||||
blogEditorInstance = new BlogEditor('editorjs', initialContent, 'travel');
|
||||
window.blogEditorInstance = blogEditorInstance;
|
||||
} catch (error) {
|
||||
console.error('Error initializing BlogEditor:', error);
|
||||
}
|
||||
|
||||
const form = document.getElementById('travelForm');
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const saveBtn = document.getElementById('saveBtn');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
|
||||
|
||||
try {
|
||||
// Lấy dữ liệu thô
|
||||
const rawData = await blogEditorInstance.save();
|
||||
|
||||
// Làm sạch dữ liệu trước khi đóng gói JSON
|
||||
const travelManager = new TravelContentManager();
|
||||
const cleanedData = travelManager.cleanEditorData(rawData);
|
||||
|
||||
const heroData = {
|
||||
title: document.getElementById('heroTitle').value.trim(),
|
||||
backgroundImage: document.getElementById('heroBackgroundImage').value.trim(),
|
||||
};
|
||||
|
||||
const pageData = {
|
||||
title: document.getElementById('pageTitle').value.trim(),
|
||||
year: document.getElementById('pageYear')?.value.trim(),
|
||||
metadata: {
|
||||
title: document.getElementById('metadataTitle').value.trim(),
|
||||
description: document.getElementById('metadataDescription').value.trim(),
|
||||
},
|
||||
};
|
||||
|
||||
document.getElementById('heroJson').value = JSON.stringify(heroData);
|
||||
document.getElementById('pageJson').value = JSON.stringify(pageData);
|
||||
document.getElementById('contentJson').value = JSON.stringify(cleanedData);
|
||||
document.getElementById('enableScrollspyInput').value = document.getElementById('enableScrollspy').checked;
|
||||
|
||||
form.submit();
|
||||
} catch (error) {
|
||||
console.error('Save error:', error);
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
|
||||
}
|
||||
});
|
||||
|
||||
// Preview
|
||||
const previewBtn = document.querySelector('.preview-btn');
|
||||
const previewModal = new bootstrap.Modal(document.getElementById('previewModal'));
|
||||
previewBtn.addEventListener('click', async function () {
|
||||
try {
|
||||
const editorData = await blogEditorInstance.save();
|
||||
const travelManager = new TravelContentManager();
|
||||
const cleanedData = travelManager.cleanEditorData(editorData);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('content', JSON.stringify(cleanedData));
|
||||
formData.append('heroTitle', document.getElementById('heroTitle').value);
|
||||
formData.append('heroBackgroundImage', document.getElementById('heroBackgroundImage').value);
|
||||
formData.append('pageTitle', document.getElementById('pageTitle').value);
|
||||
formData.append('pageYear', document.getElementById('pageYear')?.value || '');
|
||||
|
||||
const response = await fetch('/admin/travel/preview', { method: 'POST', body: formData });
|
||||
const html = await response.text();
|
||||
const previewFrame = document.getElementById('previewFrame');
|
||||
const blob = new Blob([html], { type: 'text/html' });
|
||||
previewFrame.src = URL.createObjectURL(blob);
|
||||
previewModal.show();
|
||||
} catch (error) {
|
||||
console.error('Preview error:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Image Upload Helpers
|
||||
document.querySelectorAll('.btn-upload-image').forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
document.getElementById('currentImageType').value = this.getAttribute('data-image-type');
|
||||
document.getElementById('currentTargetInput').value = this.getAttribute('data-target-input');
|
||||
document.getElementById('directImageUpload').click();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('directImageUpload').addEventListener('change', async function () {
|
||||
if (!this.files || !this.files[0]) return;
|
||||
const formData = new FormData();
|
||||
formData.append('image', this.files[0]);
|
||||
const imageType = document.getElementById('currentImageType').value;
|
||||
const targetInput = document.getElementById('currentTargetInput').value;
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/admin/upload/image?imageType=${imageType}`, { method: 'POST', body: formData });
|
||||
const result = await resp.json();
|
||||
if (result.success) {
|
||||
document.getElementById(targetInput).value = result.path;
|
||||
if (targetInput === 'heroBackgroundImage') updateHeroImagePreview(result.path);
|
||||
showToast('Uploaded successfully', 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Upload failed', 'danger');
|
||||
}
|
||||
});
|
||||
|
||||
function updateHeroImagePreview(imageUrl) {
|
||||
document.getElementById('heroImagePreview').innerHTML = `<img src="${imageUrl}" class="img-thumbnail" style="height: 200px; width: 100%; object-fit: cover;">`;
|
||||
}
|
||||
|
||||
function showToast(message, type) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast align-items-center text-bg-${type} border-0 position-fixed bottom-0 end-0 m-3`;
|
||||
toast.innerHTML = `<div class="d-flex"><div class="toast-body">${message}</div><button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
|
||||
document.body.appendChild(toast);
|
||||
new bootstrap.Toast(toast, { delay: 3000 }).show();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user