fix: add leading slashes to image paths in service data

This commit is contained in:
nguyenvanbao
2026-02-04 16:25:54 +07:00
parent c67ed49a03
commit 6294920e68
3 changed files with 130 additions and 47 deletions

View File

@@ -11,17 +11,17 @@
"slug": "immigration-appeal",
"name": "Immigration Appeal & Legal Support",
"description": "Our experts provide professional guidance for immigration appeals and legal matters, helping clients overcome visa rejections with personalized strategies and strong case representation.",
"image": "img/home-3/service/01.jpg",
"image": "/img/home-3/service/01.jpg",
"layout": "left",
"details": {
"title": "Immigration Appeal & Legal Support",
"description": "Our experts provide professional guidance for immigration appeals and legal matters, helping clients overcome visa rejections with personalized strategies and strong case representation. We analyze your case thoroughly and develop custom strategies to maximize your chances of success.",
"mainImage": "img/inner-page/service-details/details-1.jpg",
"mainImage": "/img/inner-page/service-details/details-1.jpg",
"overviewTitle": "Service Overview",
"overviewDescription": "Our Immigration Appeal & Legal Support service is designed to help clients navigate complex immigration challenges. We provide expert legal guidance, case analysis, and strategic representation to maximize your chances of success. With our expert consultants, personalized approach, and global network, we ensure a smooth transition for every client.",
"additionalDescription": "From start to finish, we are committed to turning your immigration challenges into success stories through professional legal representation and strategic planning.",
"keyFeaturesTitle": "Key Features",
"keyFeaturesImage": "img/inner-page/service-details/details-2.jpg",
"keyFeaturesImage": "/img/inner-page/service-details/details-2.jpg",
"features": [
{
"title": "Personalized Guidance",
@@ -49,7 +49,7 @@
}
],
"faqTitle": "Frequently Asked Question",
"faqImage": "img/inner-page/service-details/details-3.jpg",
"faqImage": "/img/inner-page/service-details/details-3.jpg",
"faq": [
{
"id": "faq-appeal-1",
@@ -82,17 +82,17 @@
"slug": "scholarship-guidance",
"name": "Scholarship & Study Grant Guidance",
"description": "We help students unlock opportunities to study abroad with the right financial support. Our expert advisors guide you in finding scholarships, grants, and funding options that match your academic background, chosen destination, and career goals.",
"image": "img/home-3/service/02.jpg",
"image": "/img/home-3/service/02.jpg",
"layout": "right",
"details": {
"title": "Scholarship & Study Grant Guidance",
"description": "We help students unlock opportunities to study abroad with the right financial support. Our expert advisors guide you in finding scholarships, grants, and funding options that match your academic background, chosen destination, and career goals. From preparing strong applications to meeting eligibility criteria, we ensure you maximize your chances of securing financial aid.",
"mainImage": "img/inner-page/service-details/details-1.jpg",
"mainImage": "/img/inner-page/service-details/details-1.jpg",
"overviewTitle": "Service Overview",
"overviewDescription": "Our Education Visa Consultancy is dedicated to guiding students in achieving their study abroad dreams. We provide complete support including university selection, application assistance, scholarship guidance, visa documentation, and interview preparation. With our expert consultants, personalized approach, and global network, we ensure a smooth transition for every student.",
"additionalDescription": "From start to finish, we are committed to turning your education journey into a successful international experience.",
"keyFeaturesTitle": "Key Features",
"keyFeaturesImage": "img/inner-page/service-details/details-2.jpg",
"keyFeaturesImage": "/img/inner-page/service-details/details-2.jpg",
"features": [
{
"title": "Personalized Guidance",
@@ -120,7 +120,7 @@
}
],
"faqTitle": "Frequently Asked Question",
"faqImage": "img/inner-page/service-details/details-3.jpg",
"faqImage": "/img/inner-page/service-details/details-3.jpg",
"faq": [
{
"id": "faq-scholarship-1",
@@ -153,17 +153,17 @@
"slug": "permanent-residency",
"name": "Permanent Residency (PR) Services",
"description": "Our PR services guide clients through every step of the residency process, including documentation, eligibility assessment, and application support, ensuring a smooth and successful approval.",
"image": "img/home-3/service/03.jpg",
"image": "/img/home-3/service/03.jpg",
"layout": "left",
"details": {
"title": "Permanent Residency (PR) Services",
"description": "Our PR services guide clients through every step of the residency process, including documentation, eligibility assessment, and application support, ensuring a smooth and successful approval.",
"mainImage": "img/inner-page/service-details/details-1.jpg",
"mainImage": "/img/inner-page/service-details/details-1.jpg",
"overviewTitle": "Service Overview",
"overviewDescription": "Our Permanent Residency services provide comprehensive support for individuals seeking to establish permanent residence in their chosen country. We handle all aspects of the PR application process with expertise and care.",
"additionalDescription": "Our experienced team ensures that your PR application is handled professionally and efficiently, maximizing your chances of approval.",
"keyFeaturesTitle": "Key Features",
"keyFeaturesImage": "img/inner-page/service-details/details-2.jpg",
"keyFeaturesImage": "/img/inner-page/service-details/details-2.jpg",
"features": [
{
"title": "Eligibility Assessment",
@@ -191,7 +191,7 @@
}
],
"faqTitle": "Frequently Asked Question",
"faqImage": "img/inner-page/service-details/details-3.jpg",
"faqImage": "/img/inner-page/service-details/details-3.jpg",
"faq": [
{
"id": "faq-pr-1",
@@ -224,17 +224,17 @@
"slug": "citizenship-naturalization",
"name": "Citizenship & Naturalization Guidance",
"description": "We provide expert guidance for citizenship and naturalization processes, assisting clients with documentation, eligibility, and legal procedures to achieve a smooth and successful application.",
"image": "img/home-3/service/04.jpg",
"image": "/img/home-3/service/04.jpg",
"layout": "right",
"details": {
"title": "Citizenship & Naturalization Guidance",
"description": "We provide expert guidance for citizenship and naturalization processes, assisting clients with documentation, eligibility, and legal procedures to achieve a smooth and successful application.",
"mainImage": "img/inner-page/service-details/details-1.jpg",
"mainImage": "/img/inner-page/service-details/details-1.jpg",
"overviewTitle": "Service Overview",
"overviewDescription": "Our Citizenship & Naturalization service helps individuals navigate the complex process of becoming a citizen. We provide step-by-step guidance, documentation support, and legal expertise throughout the entire process.",
"additionalDescription": "With our comprehensive approach, we make the path to citizenship clear, manageable, and successful for every client.",
"keyFeaturesTitle": "Key Features",
"keyFeaturesImage": "img/inner-page/service-details/details-2.jpg",
"keyFeaturesImage": "/img/inner-page/service-details/details-2.jpg",
"features": [
{
"title": "Citizenship Test Preparation",
@@ -262,7 +262,7 @@
}
],
"faqTitle": "Frequently Asked Question",
"faqImage": "img/inner-page/service-details/details-3.jpg",
"faqImage": "/img/inner-page/service-details/details-3.jpg",
"faq": [
{
"id": "faq-citizenship-1",
@@ -295,7 +295,7 @@
},
"destinations": {
"backgroundImage": "img/home-3/choose-us/bg.png",
"backgroundImage": "/img/home-3/choose-us/bg.png",
"title": {
"subTitle": "Countries we offer",
"mainTitle": "Choose Your Immigration Destination"
@@ -336,7 +336,7 @@
"subTitle": "What Our Clients Say",
"mainTitle": "Immigration Success Stories"
},
"thumb": "img/home-3/test-thumb.jpg",
"thumb": "/img/home-3/test-thumb.jpg",
"items": [
{
"id": "client-review-1",

View File

@@ -149,7 +149,7 @@
</div>
<!-- Features List -->
<div class="row">
<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>
@@ -228,7 +228,7 @@
</div>
<!-- FAQ List -->
<div class="row">
<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>

View File

@@ -156,7 +156,7 @@
<a href="/admin/service/<%= service.slug %>/details" class="btn btn-sm btn-outline-warning" title="Edit Details">
<i class="fas fa-cog"></i>
</a>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteService(<%= index %>)" title="Delete Service">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteService('<%= service.slug %>')" title="Delete Service">
<i class="fas fa-trash"></i>
</button>
</div>
@@ -570,18 +570,32 @@ function addService() {
showSuccess('Service added successfully!');
}
function deleteService(index) {
const service = servicesData[index];
if (!service) return;
function deleteService(slug) {
const serviceIndex = servicesData.findIndex(service => service && service.slug === slug);
if (serviceIndex === -1) {
showError('Service not found.');
return;
}
const service = servicesData[serviceIndex];
// Set service name in modal
document.getElementById('deleteServiceName').textContent = service.name || 'Unnamed Service';
const serviceNameElement = document.getElementById('deleteServiceName');
if (serviceNameElement) {
serviceNameElement.textContent = service.name || 'Unnamed Service';
}
// Show custom modal
showDeleteModal();
// Handle confirm delete
const confirmBtn = document.getElementById('confirmDeleteBtn');
if (!confirmBtn) {
showError('Delete button not found.');
return;
}
// Remove any existing event listeners by cloning the button
const newConfirmBtn = confirmBtn.cloneNode(true);
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
@@ -593,36 +607,54 @@ function deleteService(index) {
// 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;
if (cancelBtn) cancelBtn.disabled = true;
if (closeBtn) closeBtn.disabled = true;
// Add loading overlay to modal
showModalLoading();
try {
// Simulate API call delay for better UX
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise(resolve => setTimeout(resolve, 800));
// Check if service still exists (in case of concurrent modifications)
const currentServiceIndex = servicesData.findIndex(service => service && service.slug === slug);
if (currentServiceIndex === -1) {
throw new Error('Service no longer exists.');
}
// Store service name for success message
const serviceName = servicesData[currentServiceIndex].name || 'Unnamed Service';
// Perform delete
servicesData.splice(index, 1);
servicesData.splice(currentServiceIndex, 1);
updateServicesTable();
updateStatistics();
// Hide loading overlay first
hideModalLoading();
// Reset button states
this.disabled = false;
this.innerHTML = '<i class="fas fa-trash me-2"></i>Delete Service';
if (cancelBtn) cancelBtn.disabled = false;
if (closeBtn) closeBtn.disabled = false;
// Hide modal
closeDeleteModal();
// Show success message
showSuccess(`Service "${service.name}" deleted successfully!`);
showSuccess(`Service "${serviceName}" deleted successfully!`);
} catch (error) {
console.error('Error deleting service:', error);
showError('Failed to delete service. Please try again.');
} finally {
// Reset button states
// Reset button states on error
this.disabled = false;
this.innerHTML = '<i class="fas fa-trash me-2"></i>Delete Service';
cancelBtn.disabled = false;
closeBtn.disabled = false;
if (cancelBtn) cancelBtn.disabled = false;
if (closeBtn) closeBtn.disabled = false;
// Hide loading overlay
hideModalLoading();
@@ -632,6 +664,11 @@ function deleteService(index) {
function showModalLoading() {
const modal = document.getElementById('customDeleteModal');
if (!modal) return;
// Remove any existing loading overlay first
hideModalLoading();
const loadingOverlay = document.createElement('div');
loadingOverlay.className = 'modal-loading-overlay';
loadingOverlay.innerHTML = `
@@ -640,7 +677,11 @@ function showModalLoading() {
<p class="mt-2 mb-0">Deleting service...</p>
</div>
`;
modal.appendChild(loadingOverlay);
const modalContent = modal.querySelector('.custom-modal-content');
if (modalContent) {
modalContent.appendChild(loadingOverlay);
}
}
function hideModalLoading() {
@@ -650,8 +691,36 @@ function hideModalLoading() {
}
}
function closeDeleteModal() {
const modal = document.getElementById('customDeleteModal');
if (!modal) return;
// Check if loading - don't close if loading
if (document.querySelector('.modal-loading-overlay')) {
return;
}
modal.classList.remove('show');
setTimeout(() => {
modal.style.display = 'none';
document.body.style.overflow = '';
}, 300);
// Remove ESC key listener
document.removeEventListener('keydown', handleEscKey);
// Clean up any existing event listeners
const backdrop = modal.querySelector('.custom-modal-backdrop');
if (backdrop) {
backdrop.onclick = null;
}
}
function showDeleteModal() {
const modal = document.getElementById('customDeleteModal');
if (!modal) return;
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
@@ -662,11 +731,14 @@ function showDeleteModal() {
// 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();
}
};
if (backdrop) {
backdrop.onclick = function(e) {
// Only close if clicking the backdrop itself, not its children
if (e.target === backdrop && !document.querySelector('.modal-loading-overlay')) {
closeDeleteModal();
}
};
}
// Close modal with ESC key (only if not loading)
document.addEventListener('keydown', handleEscKey);
@@ -728,7 +800,7 @@ function updateServicesTable() {
<a href="/admin/service/${service.slug || 'unknown'}/details" class="btn btn-sm btn-outline-warning" title="Edit Details">
<i class="fas fa-cog"></i>
</a>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteService(${index})" title="Delete Service">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteService('${service.slug || 'unknown'}')" title="Delete Service">
<i class="fas fa-trash"></i>
</button>
</div>
@@ -963,6 +1035,7 @@ function showError(message) {
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
overflow: hidden;
position: relative;
}
.custom-modal-header {
@@ -970,7 +1043,7 @@ function showError(message) {
padding: 20px 25px 15px;
border-bottom: 1px solid #dee2e6;
display: flex;
justify-content: between;
justify-content: space-between;
align-items: center;
}
@@ -995,13 +1068,20 @@ function showError(message) {
align-items: center;
justify-content: center;
transition: all 0.2s ease;
flex-shrink: 0;
}
.custom-modal-close:hover {
.custom-modal-close:hover:not(:disabled) {
background: rgba(0, 0, 0, 0.1);
color: #495057;
}
.custom-modal-close:disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
.custom-modal-body {
padding: 25px;
text-align: center;
@@ -1094,9 +1174,12 @@ function showError(message) {
transform: none !important;
}
.custom-modal-close:disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
/* Ensure modal content has relative positioning for overlay */
.custom-modal-content {
background: white;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
overflow: hidden;
position: relative;
}
</style>