Files

1614 lines
75 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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)">Footer Management</h1>
<p class="text-muted mb-0">Edit footer content and structure</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="content-with-fixed-buttons">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="footerJson" id="footerJson" />
<input type="hidden" name="activeTab" id="activeTabInput" value="about" />
<!-- 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="#about" role="tab">
<i class="fas fa-info-circle me-2"></i>About
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#logo" role="tab">
<i class="fas fa-image me-2"></i>Logo
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#contact" role="tab">
<i class="fas fa-address-book me-2"></i>Contact & Address
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#social" role="tab">
<i class="fas fa-share-alt me-2"></i>Social Links
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#copyright" role="tab">
<i class="fas fa-copyright me-2"></i>Copyright
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- About Tab -->
<div class="tab-pane fade show active" id="about" role="tabpanel">
<div class="row g-4">
<!-- Background Image -->
<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-image me-2"></i>Background Image</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Background Image URL</label>
<div class="input-group mb-2">
<input
type="text"
class="form-control"
id="topBgImage"
name="top[bgImage]"
value=""
placeholder="/assets/img/home-1/footer-bg.jpg"
/>
<button
type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="topBgImage"
data-image-type="header"
>
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
<div class="col-md-6" id="bgImagePreviewContainer">
<!-- Background preview will be populated here -->
</div>
</div>
</div>
</div>
</div>
<!-- Menu Links -->
<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 me-2"></i>Top Menu Links</h6>
<button
type="button"
class="btn btn-primary btn-sm"
id="addTopMenuLink"
>
<i class="fas fa-plus me-1"></i>Add Menu Link
</button>
</div>
<div class="card-body">
<div id="topMenuLinksContainer" class="menu-links-sortable">
<!-- Menu links will be populated here -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Logo Tab -->
<div class="tab-pane fade" id="logo" 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-image me-2"></i>Logo Configuration
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Logo Image</label>
<div class="input-group mb-2">
<input
type="text"
class="form-control"
id="topLogoSrc"
name="top[logo][src]"
value=""
placeholder="/assets/img/logo/white-logo.svg"
/>
<button
type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="topLogoSrc"
data-image-type="header"
>
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<small class="text-muted">Recommended size: 200x60px</small>
</div>
<div class="col-md-6" id="logoPreviewContainer">
<!-- Logo preview will be populated here -->
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-6">
<label class="form-label fw-medium">Logo Alt Text</label>
<input
type="text"
class="form-control"
id="topLogoAlt"
name="top[logo][alt]"
value=""
placeholder="logo"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Logo Link</label>
<input
type="text"
class="form-control"
id="topLogoHref"
name="top[logo][href]"
value=""
placeholder="/"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Contact & Address Tab -->
<div class="tab-pane fade" id="contact" role="tabpanel">
<div class="row g-4">
<!-- Phone Information -->
<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-phone me-2"></i>Phone Information</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Phone Display</label>
<input
type="text"
class="form-control"
id="topPhoneDisplay"
name="top[phone][display]"
value=""
placeholder="+84 961 83 4040"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Phone Link</label>
<input
type="text"
class="form-control"
id="topPhoneHref"
name="top[phone][href]"
value=""
placeholder="tel:+84961834040"
/>
</div>
</div>
</div>
</div>
</div>
<!-- Address Information -->
<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-map-marker-alt me-2"></i>Address Information
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Address</label>
<input
type="text"
class="form-control"
id="topAddress"
name="top[address]"
value=""
placeholder="734 Luy Ban Bich St, Tan Thanh Ward, Tan Phu Dist, HCMC"
/>
</div>
</div>
</div>
</div>
</div>
<!-- Bottom Menu Links -->
<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 me-2"></i>Bottom Menu Links</h6>
<button
type="button"
class="btn btn-primary btn-sm"
id="addBottomMenuLink"
>
<i class="fas fa-plus me-1"></i>Add Menu Link
</button>
</div>
<div class="card-body">
<div id="bottomMenuLinksContainer" class="menu-links-sortable">
<!-- Bottom menu links will be populated here -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Social Links Tab -->
<div class="tab-pane fade" id="social" role="tabpanel">
<div class="row g-4">
<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-share-alt me-2"></i>Social Media Links
</h6>
<button
type="button"
class="btn btn-primary btn-sm"
id="addTopSocialLink"
>
<i class="fas fa-plus me-1"></i>Add Social Link
</button>
</div>
<div class="card-body">
<div id="topSocialLinksContainer" class="social-links-sortable">
<!-- Social links will be populated here -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Copyright Tab -->
<div class="tab-pane fade" id="copyright" 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-copyright me-2"></i>Copyright Information
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label fw-medium">Copyright Text</label>
<input
type="text"
class="form-control"
id="bottomCopyrightText"
name="bottom[copyright][text]"
value=""
placeholder="Copyright©"
/>
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Brand Name</label>
<input
type="text"
class="form-control"
id="bottomCopyrightBrand"
name="bottom[copyright][brand]"
value=""
placeholder="GRAMENTHEME"
/>
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Rights Text</label>
<input
type="text"
class="form-control"
id="bottomCopyrightRights"
name="bottom[copyright][rights]"
value=""
placeholder="All Rights Reserved."
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Fixed actions for ALL tabs -->
<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" id="footerResetBtn">
<i class="fas fa-undo me-1"></i>Reset
</button>
<button type="button" id="saveFooterBtn" class="btn btn-outline-primary px-4">
<i class="fas fa-save me-1"></i>Save Changes
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal for Add Top Menu Link (Static - Outside tabs to prevent z-index issues) -->
<div class="modal fade" id="addTopMenuLinkModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg">
<div class="modal-header bg-light border-bottom-0 py-3">
<h5 class="modal-title fw-bold"><i class="fas fa-plus me-2"></i>Add Top Menu Link</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<div class="mb-3">
<label for="topMenuLinkLabel" class="form-label fw-medium">Label</label>
<input
type="text"
class="form-control form-control-lg fs-6"
id="topMenuLinkLabel"
placeholder="Home"
required
/>
<small class="text-muted">Display text for the menu link.</small>
</div>
<div class="mb-3">
<label for="topMenuLinkHref" class="form-label fw-medium">URL</label>
<div class="input-group">
<span class="input-group-text bg-light"><i class="fas fa-link"></i></span>
<input type="text" class="form-control" id="topMenuLinkHref" placeholder="/" required />
</div>
<small class="text-muted">Use relative paths for internal links.</small>
</div>
</div>
<div class="modal-footer bg-light border-top-0 py-3">
<button type="button" class="btn btn-white border px-4" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary px-4" id="confirmAddTopMenuLink">
<i class="fas fa-plus me-1"></i>Add Link
</button>
</div>
</div>
</div>
</div>
<!-- Modal for Add Top Social Link (Static - Outside tabs to prevent z-index issues) -->
<div class="modal fade" id="addTopSocialLinkModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg">
<div class="modal-header bg-light border-bottom-0 py-3">
<h5 class="modal-title fw-bold"><i class="fas fa-plus me-2"></i>Add Social Link</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<div class="mb-3">
<label for="topSocialLinkIcon" class="form-label fw-medium">Icon Class</label>
<input
type="text"
class="form-control form-control-lg fs-6"
id="topSocialLinkIcon"
placeholder="fa-brands fa-twitter"
required
/>
<small class="text-muted"
>Examples: fa-brands fa-facebook, fa-brands fa-twitter, fa-brands fa-instagram</small
>
</div>
<div class="mb-3">
<label for="topSocialLinkHref" class="form-label fw-medium">URL</label>
<div class="input-group">
<span class="input-group-text bg-light"><i class="fas fa-link"></i></span>
<input
type="url"
class="form-control"
id="topSocialLinkHref"
placeholder="https://twitter.com/yourhandle"
required
/>
</div>
<small class="text-muted">Full URL including https://</small>
</div>
</div>
<div class="modal-footer bg-light border-top-0 py-3">
<button type="button" class="btn btn-white border px-4" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary px-4" id="confirmAddTopSocialLink">
<i class="fas fa-plus me-1"></i>Add Link
</button>
</div>
</div>
</div>
</div>
<!-- Modal for Add Bottom Menu Link (Static - Outside tabs to prevent z-index issues) -->
<div class="modal fade" id="addBottomMenuLinkModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg">
<div class="modal-header bg-light border-bottom-0 py-3">
<h5 class="modal-title fw-bold"><i class="fas fa-plus me-2"></i>Add Bottom Menu Link</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<div class="mb-3">
<label for="bottomMenuLinkLabel" class="form-label fw-medium">Label</label>
<input
type="text"
class="form-control form-control-lg fs-6"
id="bottomMenuLinkLabel"
placeholder="Terms & Conditions"
required
/>
<small class="text-muted">Display text for the menu link.</small>
</div>
<div class="mb-3">
<label for="bottomMenuLinkHref" class="form-label fw-medium">URL</label>
<div class="input-group">
<span class="input-group-text bg-light"><i class="fas fa-link"></i></span>
<input
type="text"
class="form-control"
id="bottomMenuLinkHref"
placeholder="/contact"
required
/>
</div>
<small class="text-muted">Use relative paths for internal links.</small>
</div>
</div>
<div class="modal-footer bg-light border-top-0 py-3">
<button type="button" class="btn btn-white border px-4" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary px-4" id="confirmAddBottomMenuLink">
<i class="fas fa-plus me-1"></i>Add Link
</button>
</div>
</div>
</div>
</div>
<style>
.content-with-fixed-buttons {
padding-bottom: 20px;
}
.menu-links-sortable .card,
.social-links-sortable .card {
transition: transform 0.2s ease;
}
.drag-handle {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
border-radius: 4px;
transition: all 0.2s ease;
cursor: grab !important;
}
.drag-handle:active {
cursor: grabbing !important;
}
.drag-handle:hover {
background-color: #f0f0f0;
color: #0d6efd;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
/* Modal enhancements - Based on Header structure */
.modal-content {
border: none;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.modal-header {
background: #f8f9fa;
border-bottom: none;
}
.modal-body {
padding: 2rem;
}
.modal-footer {
background: #f8f9fa;
border-top: none;
padding: 1rem 2rem;
}
.modal-title {
font-weight: 600;
}
.form-control-lg {
font-size: 1rem;
}
.btn-white {
background-color: #fff;
border-color: #dee2e6;
color: #6c757d;
}
.btn-white:hover {
background-color: #f8f9fa;
border-color: #adb5bd;
color: #495057;
}
/* Fix Modal Freeze & Z-Index issues */
body.modal-open {
overflow: hidden !important;
padding-right: 0 !important;
}
/* SortableJS Classes for Bottom Menu Links - Copy from Header Menu pattern */
.menu-ghost {
opacity: 0.4;
border: 2px dashed #0d6efd !important;
background-color: #f8f9fa !important;
}
.menu-chosen {
background-color: #eef3ff !important;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1) !important;
}
.menu-dragging {
opacity: 0.9;
}
/* SortableJS Classes for Social Links */
.social-ghost {
opacity: 0.4;
border: 2px dashed #0d6efd !important;
background-color: #f8f9fa !important;
}
.social-chosen {
background-color: #eef3ff !important;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1) !important;
}
.social-drag {
opacity: 0.9;
}
</style>
<script>
let footerData = {};
let topMenuLinkIndex = 0;
let topSocialLinkIndex = 0;
let bottomMenuLinkIndex = 0;
async function loadFooterData() {
try {
console.log("Fetching footer data from /api/footer...");
const response = await fetch("/api/footer");
console.log("Response status:", response.status);
console.log("Response ok:", response.ok);
if (!response.ok) {
throw new Error(`Failed to load footer data: ${response.status} ${response.statusText}`);
}
footerData = await response.json();
console.log("Footer data received:", footerData);
populateForm(footerData);
console.log("Form populated successfully");
} catch (error) {
console.error("Error loading footer data:", error);
showToast("Error loading footer data: " + error.message, "error");
}
}
function populateForm(data) {
console.log("Populating form with data:", data);
if (data.top) {
console.log("Populating top section:", data.top);
document.getElementById("topBgImage").value = data.top.bgImage || "";
if (data.top.phone) {
document.getElementById("topPhoneDisplay").value = data.top.phone.display || "";
document.getElementById("topPhoneHref").value = data.top.phone.href || "";
}
document.getElementById("topAddress").value = data.top.address || "";
if (data.top.logo) {
document.getElementById("topLogoSrc").value = data.top.logo.src || "";
document.getElementById("topLogoAlt").value = data.top.logo.alt || "";
document.getElementById("topLogoHref").value = data.top.logo.href || "/";
if (data.top.logo.src) {
updateImagePreview("topLogoSrc", data.top.logo.src);
}
}
if (data.top.bgImage) {
updateBgImagePreview(data.top.bgImage);
}
if (data.top.menuLinks && Array.isArray(data.top.menuLinks)) {
console.log("Populating top menu links:", data.top.menuLinks);
populateTopMenuLinks(data.top.menuLinks);
}
if (data.top.socialLinks && Array.isArray(data.top.socialLinks)) {
console.log("Populating top social links:", data.top.socialLinks);
populateTopSocialLinks(data.top.socialLinks);
}
}
if (data.bottom) {
console.log("Populating bottom section:", data.bottom);
if (data.bottom.copyright) {
document.getElementById("bottomCopyrightText").value = data.bottom.copyright.text || "";
document.getElementById("bottomCopyrightBrand").value = data.bottom.copyright.brand || "";
document.getElementById("bottomCopyrightRights").value = data.bottom.copyright.rights || "";
}
if (data.bottom.menuLinks && Array.isArray(data.bottom.menuLinks)) {
console.log("Populating bottom menu links:", data.bottom.menuLinks);
populateBottomMenuLinks(data.bottom.menuLinks);
}
}
}
function populateTopMenuLinks(menuLinks) {
const container = document.getElementById("topMenuLinksContainer");
container.innerHTML = "";
menuLinks.forEach((link, index) => {
const linkHtml = createTopMenuLinkHtml(link, index);
container.insertAdjacentHTML("beforeend", linkHtml);
});
topMenuLinkIndex = menuLinks.length;
}
function populateTopSocialLinks(socialLinks) {
const container = document.getElementById("topSocialLinksContainer");
container.innerHTML = "";
socialLinks.forEach((link, index) => {
const linkHtml = createTopSocialLinkHtml(link, index);
container.insertAdjacentHTML("beforeend", linkHtml);
});
topSocialLinkIndex = socialLinks.length;
}
function populateBottomMenuLinks(menuLinks) {
const container = document.getElementById("bottomMenuLinksContainer");
container.innerHTML = "";
// Sort by order if available, otherwise maintain original order
const sortedLinks = menuLinks.sort((a, b) => {
const orderA = a.order || 0;
const orderB = b.order || 0;
return orderA - orderB;
});
sortedLinks.forEach((link, index) => {
const linkHtml = createBottomMenuLinkHtml(link, index);
container.insertAdjacentHTML("beforeend", linkHtml);
});
bottomMenuLinkIndex = sortedLinks.length;
// Initialize sortable after populating
setTimeout(() => {
initBottomMenuSortable();
}, 100);
}
function createTopMenuLinkHtml(link, index) {
return `
<div class="card mb-3 border social-link-item" data-top-menu-index="${index}">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-1 d-flex justify-content-center align-items-center w-auto">
<label class="form-label fw-medium">&nbsp;</label>
<div class="drag-handle" title="Drag to reorder" style="cursor: grab; font-size: 1.2rem; color: #999; user-select: none;">
<i class="fas fa-grip-vertical"></i>
</div>
</div>
<div class="col-md-5">
<label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" name="top[menuLinks][${index}][label]"
value="${link.label || ""}" placeholder="Home" />
</div>
<div class="col-md-5">
<label class="form-label fw-medium">URL</label>
<input type="text" class="form-control" name="top[menuLinks][${index}][href]"
value="${link.href || ""}" placeholder="/" />
</div>
<div class="col-md-1">
<label class="form-label">&nbsp;</label>
<div class="btn-group w-100" role="group">
<button type="button" class="btn btn-outline-danger btn-sm remove-top-menu-link rounded"
data-top-menu-index="${index}" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
`;
}
function createTopSocialLinkHtml(link, index) {
return `
<div class="card mb-3 border social-link-item" data-top-social-index="${index}">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-1 d-flex justify-content-center align-items-center w-auto">
<label class="form-label fw-medium">&nbsp;</label>
<div class="drag-handle" title="Drag to reorder" style="cursor: grab; font-size: 1.2rem; color: #999; user-select: none;">
<i class="fas fa-grip-vertical"></i>
</div>
</div>
<div class="col-md-5">
<label class="form-label fw-medium">Icon Class</label>
<input type="text" class="form-control" name="top[socialLinks][${index}][icon]"
value="${link.icon || ""}" placeholder="fa-brands fa-twitter" />
</div>
<div class="col-md-5">
<label class="form-label fw-medium">URL</label>
<input type="text" class="form-control" name="top[socialLinks][${index}][href]"
value="${link.href || ""}" placeholder="#" />
</div>
<div class="col-md-1">
<label class="form-label">&nbsp;</label>
<div class="btn-group w-100" role="group">
<button type="button" class="btn btn-outline-danger btn-sm remove-top-social-link rounded"
data-top-social-index="${index}" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
`;
}
function createBottomMenuLinkHtml(link, index) {
return `
<div class="card mb-3 border social-link-item" data-bottom-menu-index="${index}">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-1 d-flex justify-content-center align-items-center w-auto">
<label class="form-label fw-medium">&nbsp;</label>
<div class="drag-handle" title="Drag to reorder" style="cursor: grab; font-size: 1.2rem; color: #999; user-select: none;">
<i class="fas fa-grip-vertical"></i>
</div>
</div>
<div class="col-md-5">
<label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" name="bottom[menuLinks][${index}][label]"
value="${link.label || ""}" placeholder="Terms & Conditions" />
</div>
<div class="col-md-5">
<label class="form-label fw-medium">URL</label>
<input type="text" class="form-control" name="bottom[menuLinks][${index}][href]"
value="${link.href || ""}" placeholder="/contact" />
</div>
<div class="col-md-1">
<label class="form-label">&nbsp;</label>
<div class="btn-group w-100" role="group">
<button type="button" class="btn btn-outline-danger btn-sm remove-bottom-menu-link rounded"
data-bottom-menu-index="${index}" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
`;
}
function markChanged() {
const saveBtn = document.getElementById("saveFooterBtn");
if (saveBtn) {
saveBtn.classList.remove("btn-outline-primary");
saveBtn.classList.add("btn-primary");
}
}
function showToast(message, type = "success") {
let toastContainer = document.getElementById("toastContainer");
if (!toastContainer) {
toastContainer = document.createElement("div");
toastContainer.id = "toastContainer";
toastContainer.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 10px;
pointer-events: none;
`;
document.body.appendChild(toastContainer);
}
const toast = document.createElement("div");
const bgColor = type === "success" ? "#28a745" : type === "error" ? "#dc3545" : "#17a2b8";
const icon = type === "success" ? "✓" : type === "error" ? "✕" : "";
toast.style.cssText = `
background-color: ${bgColor};
color: white;
padding: 12px 16px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
animation: slideIn 0.3s ease-out;
pointer-events: auto;
cursor: pointer;
`;
toast.innerHTML = `
<span style="font-weight: bold; font-size: 16px;">${icon}</span>
<span>${message}</span>
`;
toastContainer.appendChild(toast);
setTimeout(() => {
toast.style.animation = "slideOut 0.3s ease-out";
setTimeout(() => toast.remove(), 300);
}, 3000);
toast.addEventListener("click", () => {
toast.style.animation = "slideOut 0.3s ease-out";
setTimeout(() => toast.remove(), 300);
});
}
document.addEventListener("DOMContentLoaded", function () {
console.log("Footer CMS page loaded, current URL:", window.location.href);
console.log("Starting to load footer data...");
// Safely remove any lingering modal backdrops on page load/navigation
function cleanupModals() {
// Basic reset
document.body.classList.remove("modal-open");
document.body.style.overflow = "";
document.body.style.paddingRight = "";
document.body.style.pointerEvents = "auto";
// Remove all backdrop/overlay elements
const selector = ".modal-backdrop, .overlay, .loading";
document.querySelectorAll(selector).forEach((el) => {
try {
el.remove();
} catch (err) {
console.warn("Error removing overlay:", err);
}
});
// Cleanup dynamically created modals that are not shown
document
.querySelectorAll(
".modal.fade:not(#addTopMenuLinkModal):not(#addTopSocialLinkModal):not(#addBottomMenuLinkModal)",
)
.forEach((m) => {
if (!m.classList.contains("show")) {
m.remove();
}
});
console.log("DOM Cleaned Up: Backdrops removed, body interaction restored.");
}
// Ensure all modals are in body root (prevents stacking context issues)
function relocateModals() {
const modals = document.querySelectorAll(".modal");
modals.forEach((modal) => {
if (modal.parentElement !== document.body) {
document.body.appendChild(modal);
}
});
}
window.cleanupModals = cleanupModals;
// Initial cleanup and relocation
relocateModals();
cleanupModals();
// Cleanup modals when any modal is hidden
document.addEventListener("hidden.bs.modal", function () {
cleanupModals();
});
loadFooterData();
// Initialize all sortable containers after data is loaded
setTimeout(() => {
initAllSortables();
}, 500);
const urlParams = new URLSearchParams(window.location.search);
const activeTabObj = urlParams.get("activeTab");
if (activeTabObj) {
const tabTrigger = document.querySelector(`a[href="#${activeTabObj}"]`);
if (tabTrigger) {
new bootstrap.Tab(tabTrigger).show();
document.getElementById("activeTabInput").value = activeTabObj;
}
}
document.querySelectorAll('a[data-bs-toggle="tab"]').forEach((tab) => {
tab.addEventListener("shown.bs.tab", function (event) {
const targetId = event.target.getAttribute("href").substring(1);
document.getElementById("activeTabInput").value = targetId;
});
});
document.getElementById("addTopMenuLink").addEventListener("click", function () {
// Clear form
document.getElementById("topMenuLinkLabel").value = "";
document.getElementById("topMenuLinkHref").value = "";
// Show modal
const modal = new bootstrap.Modal(document.getElementById("addTopMenuLinkModal"));
modal.show();
});
document.getElementById("addTopSocialLink").addEventListener("click", function () {
// Clear form
document.getElementById("topSocialLinkIcon").value = "";
document.getElementById("topSocialLinkHref").value = "";
// Show modal
const modal = new bootstrap.Modal(document.getElementById("addTopSocialLinkModal"));
modal.show();
});
document.getElementById("addBottomMenuLink").addEventListener("click", function () {
// Clear form
document.getElementById("bottomMenuLinkLabel").value = "";
document.getElementById("bottomMenuLinkHref").value = "";
// Show modal
const modal = new bootstrap.Modal(document.getElementById("addBottomMenuLinkModal"));
modal.show();
});
const footerInputs = document.querySelectorAll(
"#about input, #logo input, #contact input, #social input, #copyright input",
);
footerInputs.forEach((input) => {
input.addEventListener("input", markChanged);
input.addEventListener("change", markChanged);
});
const footerResetBtn = document.getElementById("footerResetBtn");
if (footerResetBtn) {
footerResetBtn.addEventListener("click", function () {
if (confirm("Are you sure you want to discard all unsaved changes and reset to current saved data?")) {
window.location.reload();
}
});
}
const saveFooterBtn = document.getElementById("saveFooterBtn");
if (saveFooterBtn) {
saveFooterBtn.addEventListener("click", async function (e) {
console.log("=== TRACE: saveFooterBtn Clicked ===");
const submitBtn = this;
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Saving...';
submitBtn.disabled = true;
try {
const buildFooterData = () => {
const getVal = (selector) => {
const el = document.querySelector(selector);
return el ? el.value : "";
};
const top = {
bgImage: getVal("#topBgImage"),
phone: {
display: getVal("#topPhoneDisplay"),
href: getVal("#topPhoneHref"),
},
address: getVal("#topAddress"),
logo: {
src: getVal("#topLogoSrc"),
alt: getVal("#topLogoAlt"),
href: getVal("#topLogoHref"),
},
menuLinks: [],
socialLinks: [],
};
document
.querySelectorAll("#topMenuLinksContainer > .card[data-top-menu-index]")
.forEach((card) => {
const label = card.querySelector('input[name*="[label]"]')?.value || "";
const href = card.querySelector('input[name*="[href]"]')?.value || "";
if (label || href) top.menuLinks.push({ label, href });
});
document
.querySelectorAll("#topSocialLinksContainer > .card[data-top-social-index]")
.forEach((card) => {
const icon = card.querySelector('input[name*="[icon]"]')?.value || "";
const href = card.querySelector('input[name*="[href]"]')?.value || "";
if (icon || href) top.socialLinks.push({ icon, href });
});
const bottom = {
copyright: {
text: getVal("#bottomCopyrightText"),
brand: getVal("#bottomCopyrightBrand"),
rights: getVal("#bottomCopyrightRights"),
},
menuLinks: [],
};
document
.querySelectorAll("#bottomMenuLinksContainer > .card[data-bottom-menu-index]")
.forEach((card, index) => {
const label = card.querySelector('input[name*="[label]"]')?.value || "";
const href = card.querySelector('input[name*="[href]"]')?.value || "";
if (label || href) {
bottom.menuLinks.push({
label,
href,
order: index + 1, // Add order based on current DOM position
});
}
});
return { top, bottom };
};
const payload = buildFooterData();
const response = await fetch("/api/admin/footer", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
showToast("Footer updated successfully!", "success");
footerData = result.data;
submitBtn.classList.remove("btn-primary");
submitBtn.classList.add("btn-outline-primary");
} else {
throw new Error(result.error || "Update failed");
}
} catch (err) {
console.error("Error updating footer:", err);
showToast("Error updating footer: " + err.message, "error");
} finally {
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
});
}
document.addEventListener("click", function (e) {
if (e.target.closest(".remove-top-menu-link")) {
e.target.closest(".card").remove();
markChanged();
}
if (e.target.closest(".remove-top-social-link")) {
e.target.closest(".card").remove();
markChanged();
}
if (e.target.closest(".remove-bottom-menu-link")) {
e.target.closest(".card").remove();
markChanged();
}
});
document.querySelectorAll(".btn-upload-image").forEach((button) => {
button.addEventListener("click", function () {
const targetInput = this.dataset.targetInput;
const imageType = this.dataset.imageType;
openImageUploader(targetInput, imageType);
});
});
// Modal confirm handlers
document.getElementById("confirmAddTopMenuLink").addEventListener("click", function () {
const label = document.getElementById("topMenuLinkLabel").value.trim();
const href = document.getElementById("topMenuLinkHref").value.trim();
if (!label || !href) {
showToast("Please fill in both Label and URL fields", "error");
return;
}
addTopMenuLinkToContainer(label, href);
// Hide modal
const modal = bootstrap.Modal.getInstance(document.getElementById("addTopMenuLinkModal"));
modal.hide();
showToast("Top menu link added successfully!", "success");
});
document.getElementById("confirmAddTopSocialLink").addEventListener("click", function () {
const icon = document.getElementById("topSocialLinkIcon").value.trim();
const href = document.getElementById("topSocialLinkHref").value.trim();
if (!icon || !href) {
showToast("Please fill in both Icon Class and URL fields", "error");
return;
}
addTopSocialLinkToContainer(icon, href);
// Hide modal
const modal = bootstrap.Modal.getInstance(document.getElementById("addTopSocialLinkModal"));
modal.hide();
showToast("Social link added successfully!", "success");
});
document.getElementById("confirmAddBottomMenuLink").addEventListener("click", function () {
const label = document.getElementById("bottomMenuLinkLabel").value.trim();
const href = document.getElementById("bottomMenuLinkHref").value.trim();
if (!label || !href) {
showToast("Please fill in both Label and URL fields", "error");
return;
}
addBottomMenuLinkToContainer(label, href);
// Hide modal
const modal = bootstrap.Modal.getInstance(document.getElementById("addBottomMenuLinkModal"));
modal.hide();
showToast("Bottom menu link added successfully!", "success");
});
});
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}"]`);
if (!uploadBtn) {
throw new Error("Upload button not found");
}
const response = await fetch(`/admin/upload/image?imageType=footer`, {
method: "POST",
body: formData,
});
if (!response.ok) {
if (response.status === 302 || response.status === 401) {
throw new Error("Please login to upload images. Redirecting to login page...");
}
throw new Error(`Upload failed with status: ${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || "Upload failed");
}
const input =
document.querySelector(`input[name="${targetInput}"]`) ||
document.getElementById(targetInput) ||
document.querySelector(`#${targetInput}`);
if (!input) {
throw new Error("Target input not found");
}
input.value = result.path;
if (targetInput === "topLogoSrc") {
updateImagePreview(targetInput, result.url);
} else if (targetInput === "topBgImage") {
updateBgImagePreview(result.url);
}
markChanged();
showToast("Image uploaded successfully!", "success");
document.body.removeChild(fileInput);
} catch (error) {
console.error("Upload error:", error);
if (error.message.includes("login")) {
showToast("Session expired. Please login again.", "error");
setTimeout(() => {
window.location.href = "/auth/login";
}, 2000);
} else {
showToast("Upload failed: " + error.message, "error");
}
if (document.body.contains(fileInput)) {
document.body.removeChild(fileInput);
}
}
};
fileInput.click();
}
function updateImagePreview(inputId, imageUrl) {
const previewContainer = document.getElementById("logoPreviewContainer");
if (!previewContainer) {
return;
}
let img = previewContainer.querySelector("img");
if (img) {
img.src = imageUrl;
previewContainer.style.display = "block";
} else {
img = document.createElement("img");
img.src = imageUrl;
img.className = "img-thumbnail";
img.style.maxHeight = "100px";
img.style.maxWidth = "300px";
img.style.objectFit = "contain";
img.style.backgroundColor = "#b8b76a";
img.alt = "Logo preview";
previewContainer.appendChild(img);
previewContainer.style.display = "block";
}
}
function updateBgImagePreview(imageUrl) {
const previewContainer = document.getElementById("bgImagePreviewContainer");
if (!previewContainer) {
return;
}
let img = previewContainer.querySelector("img");
if (img) {
img.src = imageUrl;
previewContainer.style.display = "block";
} else {
img = document.createElement("img");
img.src = imageUrl;
img.className = "img-thumbnail";
img.style.maxHeight = "100px";
img.style.maxWidth = "300px";
img.style.objectFit = "contain";
img.alt = "Background preview";
previewContainer.appendChild(img);
previewContainer.style.display = "block";
}
}
function addTopMenuLinkToContainer(label, href) {
const container = document.getElementById("topMenuLinksContainer");
const newLink = document.createElement("div");
newLink.className = "card mb-3 border social-link-item";
newLink.dataset.topMenuIndex = topMenuLinkIndex;
newLink.innerHTML = `
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-1 d-flex justify-content-center align-items-center w-auto">
<label class="form-label fw-medium">&nbsp;</label>
<div class="drag-handle" title="Drag to reorder" style="cursor: grab; font-size: 1.2rem; color: #999; user-select: none;">
<i class="fas fa-grip-vertical"></i>
</div>
</div>
<div class="col-md-5">
<label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" name="top[menuLinks][${topMenuLinkIndex}][label]" value="${label}" placeholder="Home">
</div>
<div class="col-md-5">
<label class="form-label fw-medium">URL</label>
<input type="text" class="form-control" name="top[menuLinks][${topMenuLinkIndex}][href]" value="${href}" placeholder="/">
</div>
<div class="col-md-1">
<label class="form-label">&nbsp;</label>
<div class="btn-group w-100" role="group">
<button type="button" class="btn btn-outline-danger btn-sm remove-top-menu-link rounded" data-top-menu-index="${topMenuLinkIndex}" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
`;
container.appendChild(newLink);
topMenuLinkIndex++;
markChanged();
}
function addTopSocialLinkToContainer(icon, href) {
const container = document.getElementById("topSocialLinksContainer");
const newLink = document.createElement("div");
newLink.className = "card mb-3 border social-link-item";
newLink.dataset.topSocialIndex = topSocialLinkIndex;
newLink.innerHTML = `
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-1 d-flex justify-content-center align-items-center w-auto">
<label class="form-label fw-medium">&nbsp;</label>
<div class="drag-handle" title="Drag to reorder" style="cursor: grab; font-size: 1.2rem; color: #999; user-select: none;">
<i class="fas fa-grip-vertical"></i>
</div>
</div>
<div class="col-md-5">
<label class="form-label fw-medium">Icon Class</label>
<input type="text" class="form-control" name="top[socialLinks][${topSocialLinkIndex}][icon]" value="${icon}" placeholder="fa-brands fa-twitter">
</div>
<div class="col-md-5">
<label class="form-label fw-medium">URL</label>
<input type="text" class="form-control" name="top[socialLinks][${topSocialLinkIndex}][href]" value="${href}" placeholder="#">
</div>
<div class="col-md-1">
<label class="form-label">&nbsp;</label>
<div class="btn-group w-100" role="group">
<button type="button" class="btn btn-outline-danger btn-sm remove-top-social-link rounded" data-top-social-index="${topSocialLinkIndex}" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
`;
container.appendChild(newLink);
topSocialLinkIndex++;
markChanged();
}
function addBottomMenuLinkToContainer(label, href) {
const container = document.getElementById("bottomMenuLinksContainer");
const newLink = document.createElement("div");
newLink.className = "card mb-3 border social-link-item";
newLink.dataset.bottomMenuIndex = bottomMenuLinkIndex;
newLink.innerHTML = `
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-1 d-flex justify-content-center align-items-center w-auto">
<label class="form-label fw-medium">&nbsp;</label>
<div class="drag-handle" title="Drag to reorder" style="cursor: grab; font-size: 1.2rem; color: #999; user-select: none;">
<i class="fas fa-grip-vertical"></i>
</div>
</div>
<div class="col-md-5">
<label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" name="bottom[menuLinks][${bottomMenuLinkIndex}][label]" value="${label}" placeholder="Terms & Conditions">
</div>
<div class="col-md-5">
<label class="form-label fw-medium">URL</label>
<input type="text" class="form-control" name="bottom[menuLinks][${bottomMenuLinkIndex}][href]" value="${href}" placeholder="/contact">
</div>
<div class="col-md-1">
<label class="form-label">&nbsp;</label>
<div class="btn-group w-100" role="group">
<button type="button" class="btn btn-outline-danger btn-sm remove-bottom-menu-link rounded" data-bottom-menu-index="${bottomMenuLinkIndex}" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
`;
container.appendChild(newLink);
bottomMenuLinkIndex++;
markChanged();
// Re-initialize sortable for the new container
initBottomMenuSortable();
}
// Initialize Sortable for Bottom Menu Links - Copy pattern from Header Menu
function initBottomMenuSortable() {
const bottomMenuContainer = document.getElementById("bottomMenuLinksContainer");
const SortableLib = window.Sortable || Sortable;
console.log("=== TRACE: Bottom Menu Sortable Init ===", {
containerExists: !!bottomMenuContainer,
sortableDefined: typeof SortableLib !== "undefined",
});
if (bottomMenuContainer && typeof SortableLib !== "undefined") {
try {
// Destroy existing sortable instance if any
if (bottomMenuContainer.sortableInstance) {
bottomMenuContainer.sortableInstance.destroy();
}
bottomMenuContainer.sortableInstance = new SortableLib(bottomMenuContainer, {
animation: 150,
handle: ".drag-handle",
ghostClass: "menu-ghost",
chosenClass: "menu-chosen",
dragClass: "menu-dragging",
forceFallback: true, // Use transition-based dragging for better compatibility
onStart: function () {
console.log("=== TRACE: Bottom Menu Drag Started ===");
},
onEnd: function (evt) {
console.log("=== TRACE: Bottom Menu Drag Ended ===", evt);
rebuildBottomMenuOrder();
markChanged();
},
});
console.log("=== TRACE: Sortable initialized for bottomMenuLinksContainer ===");
} catch (err) {
console.error("=== TRACE: Bottom Menu Sortable Init Error ===", err);
}
} else {
console.warn("SortableJS not loaded or bottomMenuLinksContainer not found");
}
}
// Rebuild order for Bottom Menu Links after drag & drop
function rebuildBottomMenuOrder() {
const container = document.getElementById("bottomMenuLinksContainer");
const cards = container.querySelectorAll(".card[data-bottom-menu-index]");
cards.forEach((card, index) => {
// Update data attribute
card.dataset.bottomMenuIndex = index;
// Update input names to maintain proper order
const labelInput = card.querySelector('input[name*="[label]"]');
const hrefInput = card.querySelector('input[name*="[href]"]');
const deleteBtn = card.querySelector(".remove-bottom-menu-link");
if (labelInput) {
labelInput.name = `bottom[menuLinks][${index}][label]`;
}
if (hrefInput) {
hrefInput.name = `bottom[menuLinks][${index}][href]`;
}
if (deleteBtn) {
deleteBtn.dataset.bottomMenuIndex = index;
}
});
console.log("=== TRACE: Bottom Menu Order Rebuilt ===");
}
// Initialize sortable for existing containers on page load
function initAllSortables() {
// Initialize Bottom Menu Links Sortable
initBottomMenuSortable();
// Initialize Top Menu Links Sortable (if needed)
const topMenuContainer = document.getElementById("topMenuLinksContainer");
const topSocialContainer = document.getElementById("topSocialLinksContainer");
const SortableLib = window.Sortable || Sortable;
if (topMenuContainer && typeof SortableLib !== "undefined") {
try {
if (topMenuContainer.sortableInstance) {
topMenuContainer.sortableInstance.destroy();
}
topMenuContainer.sortableInstance = new SortableLib(topMenuContainer, {
animation: 150,
handle: ".drag-handle",
ghostClass: "menu-ghost",
chosenClass: "menu-chosen",
dragClass: "menu-dragging",
forceFallback: true,
onEnd: function () {
markChanged();
},
});
} catch (err) {
console.error("Top Menu Sortable Init Error:", err);
}
}
if (topSocialContainer && typeof SortableLib !== "undefined") {
try {
if (topSocialContainer.sortableInstance) {
topSocialContainer.sortableInstance.destroy();
}
topSocialContainer.sortableInstance = new SortableLib(topSocialContainer, {
animation: 150,
handle: ".drag-handle",
ghostClass: "social-ghost",
chosenClass: "social-chosen",
dragClass: "social-drag",
forceFallback: true,
onEnd: function () {
markChanged();
},
});
} catch (err) {
console.error("Top Social Sortable Init Error:", err);
}
}
}
</script>