forked from UKSOURCE/cms.hailearning.edu.vn
add api headermenu and crud management
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="row g-0">
|
||||
<!-- Home -->
|
||||
<div class="col-md-4 border-end">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
@@ -26,6 +27,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header & Menu -->
|
||||
<div class="col-md-4 border-end">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
@@ -44,6 +46,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="col-md-4">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
@@ -62,6 +65,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- About Us -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
@@ -80,6 +84,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
@@ -98,6 +103,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAQ -->
|
||||
<div class="col-md-4 border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
@@ -116,6 +122,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Appointment -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
@@ -134,6 +141,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pricing -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
@@ -152,7 +160,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<!-- Terms & Conditions -->
|
||||
<div class="col-md-4 border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
@@ -164,15 +173,13 @@
|
||||
<p class="text-muted mb-0 small">Manage terms</p>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="/admin/terms-conditions"
|
||||
class="btn btn-sm btn-primary w-100 mt-2"
|
||||
>
|
||||
<a href="/admin/terms-conditions" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Travel -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
@@ -191,7 +198,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 border-top">
|
||||
<!-- Safety -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
@@ -209,7 +217,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<!-- Camp Location -->
|
||||
<div class="col-md-4 border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
@@ -221,16 +230,14 @@
|
||||
<p class="text-muted mb-0 small">Manage camp location</p>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="/admin/camp-location"
|
||||
class="btn btn-sm btn-primary w-100 mt-2"
|
||||
>
|
||||
<a href="/admin/camp-location" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 border-top">
|
||||
<!-- Activities -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
@@ -247,21 +254,14 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Services -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div
|
||||
class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: rgba(184, 183, 106, 0.1);
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="fas fa-running fa-lg"
|
||||
style="color: var(--primary-color)"
|
||||
></i>
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-concierge-bell fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Services</h5>
|
||||
@@ -273,6 +273,8 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blog -->
|
||||
<div class="col-md-4 border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
@@ -638,12 +640,12 @@
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
|
||||
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-campground" style="color: var(--primary-color);"></i>
|
||||
<i class="fas fa-blog" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>Camp Location API</span>
|
||||
<span>Blog API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/camp-location</code></td>
|
||||
<td><code>/api/blog</code></td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
@@ -651,10 +653,10 @@
|
||||
>GET</span
|
||||
>
|
||||
</td>
|
||||
<td>API to get camp location data</td>
|
||||
<td>API to get blog posts</td>
|
||||
<td>
|
||||
<a
|
||||
href="/api/camp-location"
|
||||
href="/api/blog"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
target="_blank"
|
||||
>
|
||||
@@ -687,6 +689,7 @@
|
||||
style="width: 40px; height: 40px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-info-circle" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small">Version</div>
|
||||
<div class="fw-bold" style="color: var(--primary-color)">
|
||||
@@ -770,4 +773,4 @@
|
||||
background-color: var(--primary-color) !important;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -196,7 +196,7 @@
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Footer Columns</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" id="addColumn">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="addColumn">
|
||||
<i class="fas fa-plus me-1"></i>Add Column
|
||||
</button>
|
||||
</div>
|
||||
@@ -208,10 +208,11 @@
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Column <%= columnIndex + 1 %>
|
||||
</h6>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm remove-column"
|
||||
data-column-index="<%= columnIndex %>">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<div class="btn-group-action">
|
||||
<button type="button" class="btn btn-sm remove-column" data-column-index="<%= columnIndex %>">
|
||||
<i class="fas fa-trash text-action-delete"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@@ -244,20 +245,20 @@
|
||||
name="columns[<%= columnIndex %>][links][<%= linkIndex %>][url]"
|
||||
value="<%= link.url %>" placeholder="/about-us/" />
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label form-label-sm"> </label>
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger btn-sm w-100 remove-link"
|
||||
data-column-index="<%= columnIndex %>" data-link-index="<%= linkIndex %>">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<div class="col-md-1 d-flex justify-content-end align-items-end">
|
||||
<div class="btn-group-action">
|
||||
<button type="button" class="btn btn-sm remove-link"
|
||||
data-column-index="<%= columnIndex %>" data-link-index="<%= linkIndex %>">
|
||||
<i class="fas fa-trash text-action-delete"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm add-link"
|
||||
<button type="button" class="btn btn-primary btn-sm add-link"
|
||||
data-column-index="<%= columnIndex %>">
|
||||
<i class="fas fa-plus me-1"></i>Add Link
|
||||
</button>
|
||||
@@ -276,7 +277,7 @@
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Social Media Links</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" id="addSocialLink">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="addSocialLink">
|
||||
<i class="fas fa-plus me-1"></i>Add Social Link
|
||||
</button>
|
||||
</div>
|
||||
@@ -301,12 +302,13 @@
|
||||
<input type="text" class="form-control" name="social[links][<%= index %>][icon]"
|
||||
value="<%= link.icon %>" />
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label"> </label>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm w-100 remove-social-link"
|
||||
data-social-index="<%= index %>">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<div class="col-md-1 d-flex justify-content-end align-items-end">
|
||||
<div class="btn-group-action">
|
||||
<button type="button" class="btn btn-sm remove-social-link"
|
||||
data-social-index="<%= index %>">
|
||||
<i class="fas fa-trash text-action-delete"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -341,13 +343,11 @@
|
||||
|
||||
<!-- Move buttons to fixed bottom -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary">
|
||||
<i class="fas fa-undo"></i>
|
||||
<span>Reset</span>
|
||||
<button type="reset" class="btn btn-secondary px-4">
|
||||
<i class="fas fa-undo me-1"></i>Reset
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i>
|
||||
<span>Save Changes</span>
|
||||
<button type="submit" class="btn btn-primary px-4">
|
||||
<i class="fas fa-save me-1"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -433,9 +433,11 @@
|
||||
<div class="card-header bg-light">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Column ${columnIndex + 1}</h6>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm remove-column" data-column-index="${columnIndex}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<div class="btn-group-action">
|
||||
<button type="button" class="btn btn-sm remove-column" data-column-index="${columnIndex}">
|
||||
<i class="fas fa-trash text-action-delete"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@@ -451,7 +453,7 @@
|
||||
<h6 class="fw-medium mb-3">Links</h6>
|
||||
<div class="column-links-container" data-column-index="${columnIndex}">
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm add-link" data-column-index="${columnIndex}">
|
||||
<button type="button" class="btn btn-primary btn-sm add-link" data-column-index="${columnIndex}">
|
||||
<i class="fas fa-plus me-1"></i>Add Link
|
||||
</button>
|
||||
</div>
|
||||
@@ -485,11 +487,12 @@
|
||||
<label class="form-label fw-medium">Icon Class</label>
|
||||
<input type="text" class="form-control" name="social[links][${socialLinkIndex}][icon]" value="">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label"> </label>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm w-100 remove-social-link" data-social-index="${socialLinkIndex}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<div class="col-md-1 d-flex justify-content-end align-items-end">
|
||||
<div class="btn-group-action">
|
||||
<button type="button" class="btn btn-sm remove-social-link" data-social-index="${socialLinkIndex}">
|
||||
<i class="fas fa-trash text-action-delete"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -535,11 +538,12 @@
|
||||
<label class="form-label form-label-sm">URL</label>
|
||||
<input type="text" class="form-control form-control-sm" name="columns[${columnIndex}][links][${linkIndex}][url]" value="" placeholder="/about-us/">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label form-label-sm"> </label>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm w-100 remove-link" data-column-index="${columnIndex}" data-link-index="${linkIndex}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<div class="col-md-1 d-flex justify-content-end align-items-end">
|
||||
<div class="btn-group-action">
|
||||
<button type="button" class="btn btn-sm remove-link" data-column-index="${columnIndex}" data-link-index="${linkIndex}">
|
||||
<i class="fas fa-trash text-action-delete"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
355
views/admin/header/menu.ejs
Normal file
355
views/admin/header/menu.ejs
Normal file
@@ -0,0 +1,355 @@
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-sitemap me-2"></i>Menu Structure
|
||||
</h6>
|
||||
<button class="btn btn-primary btn-sm" onclick="prepareAddMenu()">
|
||||
<i class="fas fa-plus me-1"></i>Add Root Menu
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="nestedMenuContainer" class="nested-menu-container">
|
||||
<ul id="menuRoot" class="list-unstyled menu-group" data-id="root">
|
||||
<% function renderMenu(items) { %>
|
||||
<% items.forEach(item => { %>
|
||||
<li class="menu-item-wrapper mb-2" data-id="<%= item._id %>">
|
||||
<div class="menu-item-row d-flex align-items-center p-3 rounded border hover-shadow-sm bg-white">
|
||||
<div class="menu-drag-handle me-2 text-muted">
|
||||
<i class="fas fa-grip-vertical"></i>
|
||||
</div>
|
||||
|
||||
<div class="menu-toggle-icon me-2">
|
||||
<% if (item.children && item.children.length > 0) { %>
|
||||
<button class="btn btn-sm p-0 btn-toggle-nested" type="button">
|
||||
<i class="fas fa-chevron-down transition-base text-muted" style="width: 12px;"></i>
|
||||
</button>
|
||||
<% } else { %>
|
||||
<div style="width: 24px;"></div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="menu-icon me-3">
|
||||
<i class="fas <%= (item.type === 'external' ? 'fa-external-link-alt text-info' : 'fa-link text-secondary') %>"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="fw-bold text-dark"><%= item.title %></span>
|
||||
<% if (item.type === 'external') { %>
|
||||
<span class="badge bg-light text-dark border ms-2" style="font-size: 0.7rem;">External</span>
|
||||
<% } %>
|
||||
<% if (item.status === 'inactive') { %>
|
||||
<span class="badge bg-soft-secondary ms-2">Inactive</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-soft-success ms-2">Active</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="text-muted small text-truncate" style="max-width: 300px;">
|
||||
<i class="fas fa-link me-1" style="font-size: 0.75rem;"></i><%= item.url %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-actions">
|
||||
<div class="btn-group-action">
|
||||
<button class="btn btn-sm btn-add-child" data-id="<%= item._id %>" title="Add Sub-menu">
|
||||
<i class="fas fa-plus text-action-add"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-edit-menu" data-item='<%= JSON.stringify(item).replace(/'/g, "'") %>' title="Edit">
|
||||
<i class="fas fa-edit text-action-edit"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-delete-menu" data-id="<%= item._id %>" data-title="<%= item.title %>" title="Delete">
|
||||
<i class="fas fa-trash text-action-delete"></i>
|
||||
</button>
|
||||
</div>
|
||||
<form id="delete-form-<%= item._id %>" action="/admin/header/menu/delete" method="POST" class="d-none">
|
||||
<input type="hidden" name="id" value="<%= item._id %>">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="list-unstyled menu-group mt-2 ps-4 ms-2 border-start nested-list" style="min-height: 5px;" data-id="<%= item._id %>">
|
||||
<% if (item.children && item.children.length > 0) { %>
|
||||
<% renderMenu(item.children) %>
|
||||
<% } %>
|
||||
</ul>
|
||||
</li>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
<% if (menuData.tree && menuData.tree.length > 0) { %>
|
||||
<% renderMenu(menuData.tree) %>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% if (!menuData.tree || menuData.tree.length === 0) { %>
|
||||
<div class="text-center py-5">
|
||||
<img src="/assets/img/icon/empty-state.svg" alt="Empty" style="width: 120px; opacity: 0.5;" class="mb-3">
|
||||
<p class="text-muted">No menu items found. Start by adding a root menu.</p>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Menu Tab JS Initialized');
|
||||
initSortable();
|
||||
|
||||
function initSortable() {
|
||||
const menuGroups = document.querySelectorAll('.menu-group');
|
||||
menuGroups.forEach(group => {
|
||||
new Sortable(group, {
|
||||
group: 'nested-menu',
|
||||
animation: 150,
|
||||
fallbackOnBody: true,
|
||||
swapThreshold: 0.65,
|
||||
handle: '.menu-drag-handle',
|
||||
ghostClass: 'menu-ghost',
|
||||
chosenClass: 'menu-chosen',
|
||||
dragClass: 'menu-dragging',
|
||||
onEnd: function (evt) {
|
||||
console.log('Drag ended', evt);
|
||||
// Highlight that changes need saving
|
||||
const saveBtn = document.getElementById('saveHeaderBtn');
|
||||
if (saveBtn && typeof window.markHeaderChanged === 'function') {
|
||||
window.markHeaderChanged();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Bind Edit/Add/Delete buttons
|
||||
bindMenuActions();
|
||||
|
||||
function bindMenuActions() {
|
||||
// Use event delegation for better performance and to handle re-rendered items if any
|
||||
const container = document.getElementById('nestedMenuContainer');
|
||||
if (container) {
|
||||
container.addEventListener('click', function(e) {
|
||||
const editBtn = e.target.closest('.btn-edit-menu');
|
||||
if (editBtn) {
|
||||
try {
|
||||
const item = JSON.parse(editBtn.dataset.item);
|
||||
console.log('=== TRACE: Edit Menu Clicked ===', item);
|
||||
prepareEditMenu(item);
|
||||
} catch (e) {
|
||||
console.error('Error parsing menu item data:', e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const addChildBtn = e.target.closest('.btn-add-child');
|
||||
if (addChildBtn) {
|
||||
const pid = addChildBtn.dataset.id;
|
||||
console.log('=== TRACE: Add Child Clicked ===', { pid });
|
||||
if (typeof prepareAddChild === 'function') {
|
||||
prepareAddChild(pid);
|
||||
} else {
|
||||
console.error('prepareAddChild function not found');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteBtn = e.target.closest('.btn-delete-menu');
|
||||
if (deleteBtn) {
|
||||
const id = deleteBtn.dataset.id;
|
||||
const title = deleteBtn.dataset.title;
|
||||
console.log('=== TRACE: Delete Menu Clicked ===', { id, title });
|
||||
if (confirm(`Are you sure you want to delete "${title}" and all its sub-menu items?`)) {
|
||||
const form = document.getElementById('delete-form-' + id);
|
||||
console.log('=== TRACE: Submitting Delete Form ===', form ? form.action : 'FORM NOT FOUND');
|
||||
if (form) form.submit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const toggleBtn = e.target.closest('.btn-toggle-nested');
|
||||
if (toggleBtn) {
|
||||
const wrapper = toggleBtn.closest('.menu-item-wrapper');
|
||||
const nestedUl = wrapper.querySelector('.nested-list');
|
||||
const icon = toggleBtn.querySelector('i');
|
||||
|
||||
if (nestedUl) {
|
||||
const isCollapsed = nestedUl.classList.toggle('collapsed');
|
||||
if (isCollapsed) {
|
||||
icon.style.transform = 'rotate(-90deg)';
|
||||
} else {
|
||||
icon.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function prepareAddMenu() {
|
||||
console.log('=== TRACE: prepareAddMenu Called ===');
|
||||
const form = document.getElementById('menuForm');
|
||||
if (!form) return;
|
||||
form.action = '/admin/header/menu/create';
|
||||
document.getElementById('modalTitle').innerText = 'Add Root Menu';
|
||||
document.getElementById('menuId').value = '';
|
||||
document.getElementById('parentId').value = '';
|
||||
document.getElementById('formTitle').value = '';
|
||||
document.getElementById('formUrl').value = '';
|
||||
document.getElementById('formOrder').value = '0';
|
||||
document.getElementById('formStatus').value = 'active';
|
||||
document.getElementById('typeInternal').checked = true;
|
||||
|
||||
const modalElement = document.getElementById('modalAddMenu');
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(modalElement);
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function prepareAddChild(parentId) {
|
||||
prepareAddMenu();
|
||||
document.getElementById('parentId').value = parentId;
|
||||
document.getElementById('modalTitle').innerText = 'Add Sub-Menu Item';
|
||||
}
|
||||
|
||||
function prepareEditMenu(item) {
|
||||
const form = document.getElementById('menuForm');
|
||||
if (!form) return;
|
||||
form.action = '/admin/header/menu/update/' + item._id;
|
||||
document.getElementById('modalTitle').innerText = 'Edit Menu Item';
|
||||
document.getElementById('menuId').value = item._id;
|
||||
document.getElementById('parentId').value = item.parentId || '';
|
||||
document.getElementById('formTitle').value = item.title;
|
||||
document.getElementById('formUrl').value = item.url;
|
||||
document.getElementById('formOrder').value = item.order;
|
||||
document.getElementById('formStatus').value = item.status;
|
||||
|
||||
if (item.type === 'external') {
|
||||
document.getElementById('typeExternal').checked = true;
|
||||
} else {
|
||||
document.getElementById('typeInternal').checked = true;
|
||||
}
|
||||
|
||||
const modalElement = document.getElementById('modalAddMenu');
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(modalElement);
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function collectMenuData() {
|
||||
const items = [];
|
||||
function traverse(element, parentId = null) {
|
||||
const children = element.children;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const li = children[i];
|
||||
if (li.tagName !== 'LI') continue;
|
||||
|
||||
const id = li.dataset.id;
|
||||
if (!id) continue;
|
||||
|
||||
items.push({
|
||||
id: id,
|
||||
order: i + 1,
|
||||
parentId: parentId === 'root' ? null : parentId
|
||||
});
|
||||
|
||||
const subUl = li.querySelector('.menu-group');
|
||||
if (subUl) {
|
||||
traverse(subUl, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
const rootUl = document.getElementById('menuRoot');
|
||||
if (rootUl) traverse(rootUl, 'root');
|
||||
return items;
|
||||
}
|
||||
|
||||
window.saveMenuChanges = function(showToastFlag = true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('=== TRACE: saveMenuChanges Called (Reorder) ===');
|
||||
const items = collectMenuData();
|
||||
|
||||
if (items.length === 0) {
|
||||
console.warn('No menu items found to reorder');
|
||||
return resolve({ success: true, message: 'No items' });
|
||||
}
|
||||
|
||||
const saveBtn = document.getElementById('saveHeaderBtn');
|
||||
const originalHtml = saveBtn ? saveBtn.innerHTML : '';
|
||||
|
||||
// Only manage button state if this is a direct call (not unified save)
|
||||
const manageButton = showToastFlag && saveBtn;
|
||||
|
||||
if (manageButton) {
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Saving Menu...';
|
||||
}
|
||||
|
||||
fetch('/admin/header/menu/reorder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ items: items })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
if (showToastFlag) showNotification('Menu structure saved', 'success');
|
||||
resolve(data);
|
||||
} else {
|
||||
reject(new Error(data.message));
|
||||
}
|
||||
})
|
||||
.catch(error => reject(error))
|
||||
.finally(() => {
|
||||
if (manageButton) {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = originalHtml;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.collectMenuData = collectMenuData;
|
||||
|
||||
// Global reset fallback
|
||||
window.prepareAddMenu = prepareAddMenu;
|
||||
</script>
|
||||
|
||||
<!-- Redundant buttons removed to use global buttons in index.ejs -->
|
||||
|
||||
<style>
|
||||
.nested-menu-container {
|
||||
padding: var(--spacing-3);
|
||||
}
|
||||
.menu-group {
|
||||
min-height: 10px;
|
||||
}
|
||||
.menu-item-row {
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
.menu-drag-handle { cursor: grab; padding: 5px; }
|
||||
.menu-drag-handle:active { cursor: grabbing; }
|
||||
|
||||
/* SortableJS Classes mapped to our variables */
|
||||
.menu-ghost {
|
||||
opacity: 0.4;
|
||||
border: 2px dashed var(--primary-color) !important;
|
||||
}
|
||||
.menu-chosen {
|
||||
background-color: var(--primary-soft) !important;
|
||||
}
|
||||
.menu-dragging {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Collapse/Expand Styles */
|
||||
.nested-list {
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
max-height: 2000px; /* Large enough for nested items */
|
||||
}
|
||||
.nested-list.collapsed {
|
||||
max-height: 0;
|
||||
margin-top: 0 !important;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.btn-toggle-nested i {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.transition-base {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="col-lg-6 p-5">
|
||||
<h1 class="fw-bold mb-4 text-white">API Management</h1>
|
||||
<p class="lead mb-4 text-white-50">Simple dashboard to control your APIs</p>
|
||||
<div class="d-flex gap-3">
|
||||
|
||||
@@ -120,7 +120,40 @@
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// Global function to clean up any stuck modal backdrops
|
||||
function forceCleanupModals() {
|
||||
document.body.classList.remove('modal-open');
|
||||
document.body.style.overflow = '';
|
||||
document.body.style.paddingRight = '';
|
||||
const backdrops = document.querySelectorAll('.modal-backdrop, .overlay, .loading');
|
||||
if (backdrops.length > 0) {
|
||||
console.warn('Force removing stuck backdrops:', backdrops.length);
|
||||
backdrops.forEach(el => el.remove());
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically clean up on every hide event
|
||||
document.addEventListener('hidden.bs.modal', function() {
|
||||
// Wait a tiny bit for the animation to finish
|
||||
setTimeout(forceCleanupModals, 100);
|
||||
});
|
||||
|
||||
// Watchdog: Check if backdrops exist without a visible modal every 2 seconds
|
||||
setInterval(() => {
|
||||
const visibleModals = document.querySelectorAll('.modal.show');
|
||||
if (visibleModals.length === 0) {
|
||||
const backdrops = document.querySelectorAll('.modal-backdrop');
|
||||
if (backdrops.length > 0) {
|
||||
forceCleanupModals();
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// Clean up on page load
|
||||
window.addEventListener('load', forceCleanupModals);
|
||||
</script>
|
||||
<%- script %>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -12,16 +12,18 @@
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
|
||||
<!-- Custom CSS -->
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #bc9f69;
|
||||
--primary-light: #bba57c;
|
||||
--primary-dark: #be9d5f;
|
||||
--secondary-color: #f5f5e8;
|
||||
--text-light: #black;
|
||||
}
|
||||
<!-- Global CSS Variables -->
|
||||
<link rel="stylesheet" href="/assets/css/variables.css" />
|
||||
<!-- CMS Component System -->
|
||||
<link rel="stylesheet" href="/assets/css/components/button.css" />
|
||||
<link rel="stylesheet" href="/assets/css/components/card.css" />
|
||||
<link rel="stylesheet" href="/assets/css/components/form.css" />
|
||||
<link rel="stylesheet" href="/assets/css/components/modal.css" />
|
||||
<link rel="stylesheet" href="/assets/css/components/table.css" />
|
||||
<!-- Layout Styles -->
|
||||
<link rel="stylesheet" href="/assets/css/layout.css" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
position: relative;
|
||||
@@ -864,16 +866,22 @@
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Axios for API calls -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
|
||||
<!-- SortableJS for drag and drop -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
|
||||
|
||||
<!-- Toast JS -->
|
||||
<script src="/js/toast.js"></script>
|
||||
|
||||
<!-- Flash Handler JS -->
|
||||
<script src="/js/flash-handler.js"></script>
|
||||
|
||||
<!-- Custom JS -->
|
||||
<!-- Custom JS Utilities -->
|
||||
<script src="/js/main.js"></script>
|
||||
|
||||
<!-- Custom modal -->
|
||||
<!-- Custom modal enhancement -->
|
||||
<script src="/js/custom-modal.js"></script>
|
||||
|
||||
<script>
|
||||
|
||||
Reference in New Issue
Block a user