fix: enhance FAQ management and add delete confirmation modal

This commit is contained in:
nguyenvanbao
2026-02-04 11:48:04 +07:00
parent a1984189bd
commit 37123976c6
5 changed files with 383 additions and 72 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

View File

@@ -2,7 +2,6 @@ const Service = require("../models/service");
const getServiceData = async () => { const getServiceData = async () => {
const service = await Service.findOne().sort({ updatedAt: -1 }); const service = await Service.findOne().sort({ updatedAt: -1 });
console.log("check layout", service.services.items.layout);
if (!service) { if (!service) {
return { return {

View File

@@ -249,9 +249,9 @@
</div> </div>
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">ID</label> <label class="form-label">ID (Auto-generated)</label>
<input type="text" class="form-control faq-id" <input type="text" class="form-control faq-id"
value="<%= faq.id || 'faq-' + (index + 1) %>" required> value="<%= faq.id || 'faq-' + (index + 1) %>" readonly>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Expanded by Default</label> <label class="form-label">Expanded by Default</label>
@@ -399,20 +399,22 @@ function removeFeature(button) {
function addFAQ() { function addFAQ() {
const container = document.getElementById('faqContainer'); const container = document.getElementById('faqContainer');
const newFaqId = generateFAQId();
const faqNumber = document.querySelectorAll('.faq-item').length + 1;
const faqHtml = ` const faqHtml = `
<div class="card mb-3 faq-item"> <div class="card mb-3 faq-item">
<div class="card-body"> <div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0 text-decoration-underline">FAQ ${faqIndex + 1}</h6> <h6 class="mb-0 text-decoration-underline">FAQ ${faqNumber}</h6>
<button type="button" class="btn btn-danger btn-sm" onclick="removeFAQ(this)"> <button type="button" class="btn btn-danger btn-sm" onclick="removeFAQ(this)">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
</div> </div>
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">ID</label> <label class="form-label">ID (Auto-generated)</label>
<input type="text" class="form-control faq-id" <input type="text" class="form-control faq-id"
value="faq-${faqIndex + 1}" required> value="${newFaqId}" readonly>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Expanded by Default</label> <label class="form-label">Expanded by Default</label>
@@ -441,13 +443,43 @@ function addFAQ() {
faqIndex++; faqIndex++;
} }
function updateFAQId(questionInput) {
// Không cần update ID nữa vì đã tự động theo số thứ tự
}
function removeFAQ(button) { function removeFAQ(button) {
const faqItem = button.closest('.faq-item'); const faqItem = button.closest('.faq-item');
if (faqItem) { if (faqItem) {
faqItem.remove(); 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) { function updateAllJsonInputs(data) {
// Collect basic details data // Collect basic details data
const details = { const details = {

View File

@@ -283,6 +283,43 @@
</div> </div>
</div> </div>
<!-- Custom Delete Confirmation Modal -->
<div id="customDeleteModal" class="custom-modal" style="display: none;">
<div class="custom-modal-backdrop"></div>
<div class="custom-modal-dialog">
<div class="custom-modal-content">
<div class="custom-modal-header">
<h5 class="custom-modal-title">
<i class="fas fa-exclamation-triangle text-warning me-2"></i>Confirm Delete
</h5>
<button type="button" class="custom-modal-close" onclick="closeDeleteModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="custom-modal-body">
<div class="text-center mb-3">
<div class="delete-icon">
<i class="fas fa-trash fa-2x text-danger"></i>
</div>
</div>
<h6 class="text-center mb-3">Delete Service</h6>
<p class="text-center text-muted mb-0">
Are you sure you want to delete "<strong id="deleteServiceName"></strong>"?
<br><small class="text-danger">This action cannot be undone.</small>
</p>
</div>
<div class="custom-modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeDeleteModal()">
<i class="fas fa-times me-2"></i>Cancel
</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">
<i class="fas fa-trash me-2"></i>Delete Service
</button>
</div>
</div>
</div>
</div>
<!-- Scripts --> <!-- Scripts -->
<script> <script>
let originalFormData = null; let originalFormData = null;
@@ -534,11 +571,110 @@ function addService() {
} }
function deleteService(index) { function deleteService(index) {
if (confirm('Are you sure you want to delete this service? This action cannot be undone.')) { const service = servicesData[index];
if (!service) return;
// Set service name in modal
document.getElementById('deleteServiceName').textContent = service.name || 'Unnamed Service';
// Show custom modal
showDeleteModal();
// Handle confirm delete
const confirmBtn = document.getElementById('confirmDeleteBtn');
const newConfirmBtn = confirmBtn.cloneNode(true);
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
newConfirmBtn.addEventListener('click', async function() {
// Show loading state
this.disabled = true;
this.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Deleting...';
// Disable cancel button and close button during delete
const cancelBtn = document.querySelector('.custom-modal .btn-secondary');
const closeBtn = document.querySelector('.custom-modal-close');
cancelBtn.disabled = true;
closeBtn.disabled = true;
// Add loading overlay to modal
showModalLoading();
try {
// Simulate API call delay for better UX
await new Promise(resolve => setTimeout(resolve, 1000));
// Perform delete
servicesData.splice(index, 1); servicesData.splice(index, 1);
updateServicesTable(); updateServicesTable();
updateStatistics(); updateStatistics();
showSuccess('Service deleted successfully!');
// Hide modal
closeDeleteModal();
// Show success message
showSuccess(`Service "${service.name}" deleted successfully!`);
} catch (error) {
console.error('Error deleting service:', error);
showError('Failed to delete service. Please try again.');
} finally {
// Reset button states
this.disabled = false;
this.innerHTML = '<i class="fas fa-trash me-2"></i>Delete Service';
cancelBtn.disabled = false;
closeBtn.disabled = false;
// Hide loading overlay
hideModalLoading();
}
});
}
function showModalLoading() {
const modal = document.getElementById('customDeleteModal');
const loadingOverlay = document.createElement('div');
loadingOverlay.className = 'modal-loading-overlay';
loadingOverlay.innerHTML = `
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin fa-2x text-primary"></i>
<p class="mt-2 mb-0">Deleting service...</p>
</div>
`;
modal.appendChild(loadingOverlay);
}
function hideModalLoading() {
const loadingOverlay = document.querySelector('.modal-loading-overlay');
if (loadingOverlay) {
loadingOverlay.remove();
}
}
function showDeleteModal() {
const modal = document.getElementById('customDeleteModal');
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
// Add animation
setTimeout(() => {
modal.classList.add('show');
}, 10);
// Close modal when clicking backdrop (only if not loading)
const backdrop = modal.querySelector('.custom-modal-backdrop');
backdrop.onclick = function() {
if (!document.querySelector('.modal-loading-overlay')) {
closeDeleteModal();
}
};
// Close modal with ESC key (only if not loading)
document.addEventListener('keydown', handleEscKey);
}
function handleEscKey(event) {
if (event.key === 'Escape' && !document.querySelector('.modal-loading-overlay')) {
closeDeleteModal();
} }
} }
@@ -779,4 +915,188 @@ function showError(message) {
.btn-group .btn:last-child { .btn-group .btn:last-child {
margin-right: 0; margin-right: 0;
} }
/* Custom Delete Modal Styles */
.custom-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.custom-modal.show {
opacity: 1;
}
.custom-modal-backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(2px);
}
.custom-modal-dialog {
position: relative;
z-index: 10001;
max-width: 400px;
width: 90%;
transform: scale(0.8);
transition: transform 0.3s ease;
}
.custom-modal.show .custom-modal-dialog {
transform: scale(1);
}
.custom-modal-content {
background: white;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
.custom-modal-header {
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
padding: 20px 25px 15px;
border-bottom: 1px solid #dee2e6;
display: flex;
justify-content: between;
align-items: center;
}
.custom-modal-title {
color: var(--primary-dark);
font-weight: 600;
margin: 0;
flex: 1;
}
.custom-modal-close {
background: none;
border: none;
font-size: 18px;
color: #6c757d;
cursor: pointer;
padding: 5px;
border-radius: 50%;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.custom-modal-close:hover {
background: rgba(0, 0, 0, 0.1);
color: #495057;
}
.custom-modal-body {
padding: 25px;
text-align: center;
}
.delete-icon {
background: rgba(220, 53, 69, 0.1);
border-radius: 50%;
width: 80px;
height: 80px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
}
.custom-modal-footer {
padding: 15px 25px 25px;
display: flex;
gap: 10px;
justify-content: center;
}
.custom-modal-footer .btn {
min-width: 120px;
}
.custom-modal .btn-danger {
background: linear-gradient(135deg, #dc3545, #c82333);
border: none;
transition: all 0.3s ease;
}
.custom-modal .btn-danger:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
}
.custom-modal .btn-secondary {
background: #6c757d;
border: none;
transition: all 0.3s ease;
}
.custom-modal .btn-secondary:hover {
background: #5a6268;
transform: translateY(-1px);
}
/* Modal Loading Overlay */
.modal-loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.95);
display: flex;
align-items: center;
justify-content: center;
z-index: 10002;
border-radius: 15px;
backdrop-filter: blur(2px);
}
.loading-spinner {
text-align: center;
color: var(--primary-color);
}
.loading-spinner p {
font-size: 14px;
color: #6c757d;
font-weight: 500;
}
.loading-spinner i {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Disable buttons during loading */
.custom-modal .btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.custom-modal-close:disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
</style> </style>

View File

@@ -722,11 +722,10 @@
</ul> </ul>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle <%= currentPath === '/admin/about' || currentPath === '/admin/affiliations' || currentPath === '/admin/partnerships' ? 'active' : '' %>" <a class="nav-link dropdown-toggle <%= currentPath === '/admin/about-us' || currentPath === '/admin/safety' || currentPath === '/admin/faq' || currentPath === '/admin/insurance' || currentPath === '/admin/travel' || currentPath === '/admin/terms-conditions' ? 'active' : '' %>"
href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"> href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
About About
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li> <li>
<a class="dropdown-item <%= currentPath === '/admin/about-us' ? 'active' : '' %>" <a class="dropdown-item <%= currentPath === '/admin/about-us' ? 'active' : '' %>"
@@ -737,13 +736,13 @@
href="/admin/safety">Safety</a> href="/admin/safety">Safety</a>
</li> </li>
<li> <li>
<a class="dropdown-item <%= currentPath === '/admin/FAQ' ? 'active' : '' %>" href="/admin/faq">FAQ</a> <a class="dropdown-item <%= currentPath === '/admin/faq' ? 'active' : '' %>"
href="/admin/faq">FAQ</a>
</li> </li>
<li> <li>
<a class="dropdown-item <%= currentPath === '/admin/insurance' ? 'active' : '' %>" <a class="dropdown-item <%= currentPath === '/admin/insurance' ? 'active' : '' %>"
href="/admin/insurance">Insurance</a> href="/admin/insurance">Insurance</a>
</li> </li>
<li> <li>
<a class="dropdown-item <%= currentPath === '/admin/travel' ? 'active' : '' %>" <a class="dropdown-item <%= currentPath === '/admin/travel' ? 'active' : '' %>"
href="/admin/travel">Travel</a> href="/admin/travel">Travel</a>
@@ -752,12 +751,27 @@
<a class="dropdown-item <%= currentPath === '/admin/terms-conditions' ? 'active' : '' %>" <a class="dropdown-item <%= currentPath === '/admin/terms-conditions' ? 'active' : '' %>"
href="/admin/terms-conditions">Terms & Conditions</a> href="/admin/terms-conditions">Terms & Conditions</a>
</li> </li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle <%= currentPath === '/admin/service' || currentPath === '/admin/activity' ? 'active' : '' %>"
href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Services
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item <%= currentPath === '/admin/service' ? 'active' : '' %>"
href="/admin/service">Service</a>
</li>
<li>
<a class="dropdown-item <%= currentPath === '/admin/activity' ? 'active' : '' %>"
href="/admin/activity">Activity & Booking</a>
</li>
</ul> </ul>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/contact' ? 'active' : '' %>" href="/admin/contact">Contact <a class="nav-link <%= currentPath === '/admin/contact' ? 'active' : '' %>"
Us</a> href="/admin/contact">Contact Us</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/appointment' ? 'active' : '' %>" <a class="nav-link <%= currentPath === '/admin/appointment' ? 'active' : '' %>"
@@ -771,60 +785,6 @@
<a class="nav-link <%= currentPath === '/admin/camp-location' ? 'active' : '' %>" <a class="nav-link <%= currentPath === '/admin/camp-location' ? 'active' : '' %>"
href="/admin/camp-location">Camp Location</a> href="/admin/camp-location">Camp Location</a>
</li> </li>
<li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/activity' ? 'active' : '' %>" href="/admin/activity">Activity
& Booking</a>
</li>
</ul>
<li>
<a
class="dropdown-item <%= currentPath === '/admin/travel' ? 'active' : '' %>"
href="/admin/travel"
>Travel</a
>
</li>
<li>
<a
class="dropdown-item <%= currentPath === '/admin/terms-conditions' ? 'active' : '' %>"
href="/admin/terms-conditions"
>Terms & Conditions</a
>
</li>
<li>
<a
class="dropdown-item <%= currentPath === '/admin/service' ? 'active' : '' %>"
href="/admin/service"
>Service</a>
</li>
</ul>
</li>
<li class="nav-item">
<a
class="nav-link <%= currentPath === '/admin/contact' ? 'active' : '' %>"
href="/admin/contact"
>Contact Us</a
>
</li>
<li class="nav-item">
<a
class="nav-link <%= currentPath === '/admin/camp-location' ? 'active' : '' %>"
href="/admin/camp-location"
>Camp Location</a
>
</li>
<li class="nav-item">
<a
class="nav-link <%= currentPath === '/admin/activity' ? 'active' : '' %>"
href="/admin/activity"
>Activity & Booking</a
>
</li>
</ul>
</ul> </ul>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<% if (locals.user) { %> <% if (locals.user) { %>