forked from UKSOURCE/cms.hailearning.edu.vn
- Improve aboutUs controller with better field handling - Update footer controller with expanded content management - Refine about admin view templates - Update appointment and footer admin views - Add about contract repair migration script - Update about.json seed data
1614 lines
75 KiB
Plaintext
1614 lines
75 KiB
Plaintext
<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 /admin/footer/data...");
|
||
const response = await fetch("/admin/footer/data");
|
||
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"> </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"> </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"> </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"> </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"> </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"> </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"> </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"> </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"> </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"> </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"> </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"> </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>
|