recreate project
This commit is contained in:
@@ -0,0 +1,441 @@
|
||||
<?php
|
||||
|
||||
/** @var array $rolesFromSession */
|
||||
|
||||
use App\Services\NavbarService;
|
||||
|
||||
$rolesFromSession = [];
|
||||
$rawRole = session()->get('role'); // your app uses a single role string
|
||||
if (is_array($rawRole)) $rolesFromSession = $rawRole;
|
||||
else $rolesFromSession = [$rawRole ?? 'guest'];
|
||||
|
||||
$service = new NavbarService();
|
||||
$menu = $service->getMenuForRoles($rolesFromSession);
|
||||
|
||||
function nav_url(string $path): string
|
||||
{
|
||||
$path = trim($path);
|
||||
if ($path === '' || preg_match('/[<>]/', $path)) {
|
||||
return '#';
|
||||
}
|
||||
// Treat relative paths as site_url, absolute keep as is.
|
||||
if (preg_match('#^https?://#i', $path)) return $path;
|
||||
return site_url($path);
|
||||
}
|
||||
|
||||
?>
|
||||
<style>
|
||||
/* Mobile-friendly sidebar behavior and backdrop */
|
||||
@media (max-width: 991.98px) {
|
||||
#navbarManagement.mgmt-sidebar { width: min(88vw, 360px); transform: translateX(-100%); will-change: transform; }
|
||||
html.mgmt-sidebar-open #navbarManagement.mgmt-sidebar { transform: translateX(0); }
|
||||
#mgmtSidebarHotzone { display: none !important; }
|
||||
.mgmt-sidebar-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.35); z-index: 1043; display: none; }
|
||||
html.mgmt-sidebar-open .mgmt-sidebar-backdrop { display: block; }
|
||||
body.mgmt-no-scroll { overflow: hidden; touch-action: none; }
|
||||
#navbarManagement.mgmt-sidebar .dropdown-menu { display: none !important; }
|
||||
}
|
||||
@media (min-width: 992px) { .mgmt-sidebar-backdrop { display: none !important; } }
|
||||
#navbarManagement .submenu .nav-link, #navbarManagement .mobile-submenu .nav-link { padding: .4rem .5rem; }
|
||||
#navbarManagement .submenu .dropdown-header, #navbarManagement .mobile-submenu .dropdown-header { padding: .35rem .5rem; }
|
||||
#navbarManagement .submenu-toggle, #navbarManagement .mobile-toggle { width: 100%; text-align: left; }
|
||||
|
||||
/* Arrow toggle styling – follow page style */
|
||||
.mgmt-sidebar-toggle {
|
||||
position: fixed;
|
||||
left: 10px;
|
||||
top: 72px; /* JS nudges based on header height */
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
background-color: var(--mgmt-primary);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
|
||||
z-index: 1060; /* above sidebar */
|
||||
cursor: pointer;
|
||||
transition: transform .2s ease, background-color .2s ease, opacity .2s ease;
|
||||
}
|
||||
.mgmt-sidebar-toggle:hover { background-color: var(--mgmt-primary-hover); }
|
||||
html.mgmt-sidebar-open .mgmt-sidebar-toggle i { transform: rotate(180deg); }
|
||||
/* Rotate submenu chevron when expanded */
|
||||
#navbarManagement .submenu-toggle[aria-expanded="true"] i { transform: rotate(180deg); }
|
||||
</style>
|
||||
|
||||
<button id="mgmtSidebarToggle" class="mgmt-sidebar-toggle" aria-label="Toggle sidebar" aria-expanded="false">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</button>
|
||||
<nav id="navbarManagement" class="mgmt-sidebar">
|
||||
<div class="mgmt-sidebar-content">
|
||||
<ul class="nav flex-column" id="mgmtMenuList">
|
||||
<?php foreach ($menu as $item): ?>
|
||||
<?php $hasChildren = !empty($item['children']); ?>
|
||||
<?php if ($hasChildren): ?>
|
||||
<li class="nav-item dropdown dropend d-none">
|
||||
<a class="nav-link d-flex align-items-center justify-content-between dropdown-toggle" href="#" id="dd_<?= esc($item['id']) ?>" role="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" data-bs-display="static" aria-expanded="false">
|
||||
<span class="nav-label-chip flex-shrink-0"><?= esc($item['label']) ?></span>
|
||||
<i class="bi bi-chevron-right ms-2"></i>
|
||||
</a>
|
||||
<div class="dropdown-menu shadow sidebar-dropdown-menu" aria-labelledby="dd_<?= esc($item['id']) ?>">
|
||||
<?php foreach ($item['children'] as $child): ?>
|
||||
<?php if ($child['url']): ?>
|
||||
<a class="dropdown-item" href="<?= esc(nav_url($child['url']), 'attr') ?>" <?= $child['target'] ? 'target="' . esc($child['target']) . '"' : '' ?>>
|
||||
<?= esc($child['label']) ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<h6 class="dropdown-header mb-1"><?= esc($child['label']) ?></h6>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</li>
|
||||
<?php $cid = 'mnav_'.esc($item['id']); ?>
|
||||
<li class="nav-item d-block">
|
||||
<button class="nav-link d-flex align-items-center justify-content-between mobile-toggle submenu-toggle" type="button" data-bs-toggle="collapse" data-bs-target="#<?= $cid ?>" aria-controls="<?= $cid ?>" aria-expanded="false">
|
||||
<span class="nav-label-chip flex-shrink-0"><?= esc($item['label']) ?></span>
|
||||
<i class="bi bi-chevron-down ms-2"></i>
|
||||
</button>
|
||||
<div id="<?= $cid ?>" class="collapse submenu" data-bs-parent="#mgmtMenuList">
|
||||
<ul class="nav flex-column ms-2">
|
||||
<?php foreach ($item['children'] as $child): ?>
|
||||
<?php if ($child['url']): ?>
|
||||
<li class="nav-item"><a class="nav-link" href="<?= esc(nav_url($child['url']), 'attr') ?>" <?= $child['target'] ? 'target="' . esc($child['target']) . '"' : '' ?>><?= esc($child['label']) ?></a></li>
|
||||
<?php else: ?>
|
||||
<li class="nav-item"><span class="dropdown-header mb-1 text-muted small"><?= esc($child['label']) ?></span></li>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<?php else: ?>
|
||||
<li class="nav-item">
|
||||
<?php if (!empty($item['url'])): ?>
|
||||
<a class="nav-link d-flex align-items-center" href="<?= esc(nav_url($item['url']), 'attr') ?>" <?= $item['target'] ? 'target="' . esc($item['target']) . '"' : '' ?>>
|
||||
<span class="nav-label-chip flex-shrink-0"><?= esc($item['label']) ?></span>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="nav-link disabled d-flex align-items-center">
|
||||
<span class="nav-label-chip flex-shrink-0"><?= esc($item['label']) ?></span>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<?php
|
||||
// Build role switcher options when user has multiple roles
|
||||
$allRoles = array_map(fn($r)=> strtolower(trim((string)$r)), (array) (session()->get('roles') ?? []));
|
||||
$currentRole = strtolower((string) ($rawRole ?? 'guest'));
|
||||
$otherRoles = array_values(array_filter($allRoles, fn($r) => $r !== '' && $r !== $currentRole));
|
||||
|
||||
// Style settings (with sensible defaults)
|
||||
$styleCfg = config('Style');
|
||||
$sess = session();
|
||||
$currentAccent = $sess->get('style_color') ?? ($styleCfg->defaultStyle ?? 'blue');
|
||||
$currentMenu = $sess->get('menu_color') ?? ($styleCfg->defaultMenu ?? 'white');
|
||||
?>
|
||||
<div class="nav-item dropdown me-2">
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-end quick-menu" aria-labelledby="dd_tools" style="max-height: 70vh; overflow-y: auto; overflow-x: hidden;">
|
||||
<?php if (!empty($otherRoles)): ?>
|
||||
<h6 class="dropdown-header">Switch Role</h6>
|
||||
<?php foreach ($otherRoles as $r): ?>
|
||||
<form method="post" action="<?= site_url('set-role') ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="selected_role" value="<?= esc($r) ?>">
|
||||
<button type="submit" class="dropdown-item">Switch to <?= esc(ucfirst(str_replace('_', ' ', $r))) ?></button>
|
||||
</form>
|
||||
<?php endforeach; ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h6 class="dropdown-header">Style</h6>
|
||||
<a class="dropdown-item d-flex justify-content-between align-items-center" data-bs-toggle="collapse" href="#ddAccentMgmtSidebar" role="button" aria-expanded="true" aria-controls="ddAccentMgmtSidebar">
|
||||
Accent <i class="bi bi-chevron-down ms-2"></i>
|
||||
</a>
|
||||
<div class="collapse show" id="ddAccentMgmtSidebar">
|
||||
<div class="px-2 pb-2">
|
||||
<?php foreach (array_keys($styleCfg->stylePalettes ?? []) as $opt): ?>
|
||||
<?php $isActive = ($opt === $currentAccent); ?>
|
||||
<a class="dropdown-item <?= $isActive ? 'active' : '' ?>" data-style-link href="<?= site_url('ui/style?accent=' . urlencode($opt)) ?>" <?= $isActive ? 'aria-current="true"' : '' ?>>
|
||||
<?= ucfirst(esc($opt)) ?><?= $isActive ? ' <i class=\"bi bi-check-lg ms-2\"></i>' : '' ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="px-2 small text-muted">Menu</div>
|
||||
<?php foreach (array_keys($styleCfg->menuPalettes ?? []) as $opt): ?>
|
||||
<?php $isActive = ($opt === $currentMenu); ?>
|
||||
<a class="dropdown-item <?= $isActive ? 'active' : '' ?>" data-style-link href="<?= site_url('ui/style?menu=' . urlencode($opt)) ?>" <?= $isActive ? 'aria-current="true"' : '' ?>>
|
||||
<?= ucfirst(esc($opt)) ?><?= $isActive ? ' <i class=\"bi bi-check-lg ms-2\"></i>' : '' ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div id="mgmtSidebarHotzone" aria-hidden="true"></div>
|
||||
<div id="mgmtSidebarBackdrop" class="mgmt-sidebar-backdrop" aria-hidden="true"></div>
|
||||
<script>
|
||||
(function() {
|
||||
function applySidebarGeometry() {
|
||||
const header = document.querySelector('header.navbar.sticky-top, header.navbar.fixed-top') || document.querySelector('header.navbar');
|
||||
const nav = document.getElementById('navbarManagement');
|
||||
const hot = document.getElementById('mgmtSidebarHotzone');
|
||||
const toggle = document.getElementById('mgmtSidebarToggle');
|
||||
if (!nav) return;
|
||||
let top = 0;
|
||||
if (header) {
|
||||
top = Math.max(header.offsetHeight || 0, (header.getBoundingClientRect && header.getBoundingClientRect().height) || 0);
|
||||
}
|
||||
nav.style.top = top + 'px';
|
||||
nav.style.height = 'calc(100vh - ' + top + 'px)';
|
||||
// Keep sidebar above backdrop (1043) but below header (1045)
|
||||
nav.style.zIndex = 1044;
|
||||
if (hot) {
|
||||
hot.style.top = top + 'px';
|
||||
hot.style.height = 'calc(100vh - ' + top + 'px)';
|
||||
hot.style.zIndex = 1019;
|
||||
}
|
||||
if (toggle) { try { toggle.style.top = (top + 12) + 'px'; } catch(_) {} }
|
||||
}
|
||||
window.addEventListener('load', applySidebarGeometry);
|
||||
window.addEventListener('resize', applySidebarGeometry);
|
||||
document.addEventListener('DOMContentLoaded', applySidebarGeometry);
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
// Toggle sidebar open/close (button + backdrop) and lock body scroll on mobile
|
||||
(function() {
|
||||
var root = document.documentElement;
|
||||
var btn = document.getElementById('mgmtSidebarToggle');
|
||||
var backdrop = document.getElementById('mgmtSidebarBackdrop');
|
||||
if (!btn) return;
|
||||
|
||||
function setExpanded(expanded) {
|
||||
try { btn.setAttribute('aria-expanded', expanded ? 'true' : 'false'); } catch (_) {}
|
||||
try { document.body.classList.toggle('mgmt-no-scroll', expanded && !window.matchMedia('(min-width: 992px)').matches); } catch (_) {}
|
||||
}
|
||||
|
||||
function closeSidebar() {
|
||||
root.classList.remove('mgmt-sidebar-open');
|
||||
setExpanded(false);
|
||||
}
|
||||
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
var willOpen = !root.classList.contains('mgmt-sidebar-open');
|
||||
root.classList.toggle('mgmt-sidebar-open');
|
||||
setExpanded(willOpen);
|
||||
});
|
||||
if (backdrop) backdrop.addEventListener('click', function() { closeSidebar(); });
|
||||
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeSidebar(); });
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!root.classList.contains('mgmt-sidebar-open')) return;
|
||||
if (e.target.closest('#navbarManagement') || e.target.closest('#mgmtSidebarToggle')) return;
|
||||
closeSidebar();
|
||||
});
|
||||
|
||||
// Desktop: hover the arrow to reveal the sidebar (large screens only)
|
||||
// Use width-only gating to be robust across devices that misreport pointer/hover
|
||||
if (window.matchMedia('(min-width: 992px)').matches) {
|
||||
btn.addEventListener('mouseenter', function() {
|
||||
root.classList.add('mgmt-sidebar-open');
|
||||
setExpanded(true);
|
||||
});
|
||||
btn.addEventListener('mouseleave', function() {
|
||||
setTimeout(function() {
|
||||
var navHover = document.querySelector('#navbarManagement:hover');
|
||||
var btnHover = document.querySelector('#mgmtSidebarToggle:hover');
|
||||
if (!navHover && !btnHover) closeSidebar();
|
||||
}, 600);
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
// Ensure style links return to the current page
|
||||
(function() {
|
||||
document.addEventListener('click', function(e) {
|
||||
var a = e.target.closest('a[data-style-link]');
|
||||
if (!a) return;
|
||||
try {
|
||||
var u = new URL(a.getAttribute('href'), window.location.origin);
|
||||
if (!u.searchParams.get('back')) {
|
||||
u.searchParams.set('back', window.location.href);
|
||||
}
|
||||
e.preventDefault();
|
||||
window.location.assign(u.toString());
|
||||
} catch (_) {
|
||||
/* ignore */ }
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
// Hover-to-open behavior with a thin hotzone at the left edge (desktop only)
|
||||
(function() {
|
||||
if (!window.matchMedia('(hover: hover) and (pointer: fine) and (min-width: 992px)').matches) return;
|
||||
var root = document.documentElement;
|
||||
var nav = document.getElementById('navbarManagement');
|
||||
var hot = document.getElementById('mgmtSidebarHotzone');
|
||||
if (!nav || !hot) return;
|
||||
var closing;
|
||||
var CLOSE_DELAY = 600; // allow time to move into flyout
|
||||
function open() {
|
||||
root.classList.add('mgmt-sidebar-open');
|
||||
}
|
||||
|
||||
function scheduleClose() {
|
||||
if (closing) clearTimeout(closing);
|
||||
closing = setTimeout(function() {
|
||||
root.classList.remove('mgmt-sidebar-open');
|
||||
}, CLOSE_DELAY);
|
||||
}
|
||||
|
||||
function cancelClose() {
|
||||
if (closing) {
|
||||
clearTimeout(closing);
|
||||
closing = null;
|
||||
}
|
||||
}
|
||||
|
||||
hot.addEventListener('mouseenter', function() {
|
||||
cancelClose();
|
||||
open();
|
||||
});
|
||||
hot.addEventListener('mouseleave', function() {
|
||||
scheduleClose();
|
||||
});
|
||||
nav.addEventListener('mouseenter', function() {
|
||||
cancelClose();
|
||||
open();
|
||||
});
|
||||
nav.addEventListener('mouseleave', function() {
|
||||
scheduleClose();
|
||||
});
|
||||
|
||||
/* touch behavior disabled; mobile uses visible toggle */
|
||||
|
||||
// ESC closes
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
scheduleClose();
|
||||
}
|
||||
});
|
||||
|
||||
// Keep open while interacting with dropdown flyouts
|
||||
nav.addEventListener('shown.bs.dropdown', function() {
|
||||
cancelClose();
|
||||
open();
|
||||
}, true);
|
||||
nav.addEventListener('hide.bs.dropdown', function() {
|
||||
scheduleClose();
|
||||
}, true);
|
||||
nav.addEventListener('mouseenter', function(e) {
|
||||
if (e.target && e.target.classList && e.target.classList.contains('dropdown-menu')) {
|
||||
cancelClose();
|
||||
}
|
||||
}, true);
|
||||
nav.addEventListener('mouseleave', function(e) {
|
||||
if (e.target && e.target.classList && e.target.classList.contains('dropdown-menu')) {
|
||||
scheduleClose();
|
||||
}
|
||||
}, true);
|
||||
|
||||
// Expose helpers for other scripts
|
||||
try {
|
||||
window._mgmtSidebarKeepOpen = {
|
||||
open: open,
|
||||
scheduleClose: scheduleClose,
|
||||
cancelClose: cancelClose
|
||||
};
|
||||
} catch (_) {}
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
// Simple click-open guard so the sidebar is open before dropdown shows (desktop only)
|
||||
(function() {
|
||||
if (!window.matchMedia('(min-width: 992px)').matches) return;
|
||||
var nav = document.getElementById('navbarManagement');
|
||||
if (!nav) return;
|
||||
nav.addEventListener('click', function(e) {
|
||||
var t = e.target.closest('.dropdown-toggle');
|
||||
if (!t || !nav.contains(t)) return;
|
||||
var root = document.documentElement;
|
||||
if (!root.classList.contains('mgmt-sidebar-open')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
root.classList.add('mgmt-sidebar-open');
|
||||
setTimeout(function() {
|
||||
try {
|
||||
new bootstrap.Dropdown(t).show();
|
||||
} catch (_) {
|
||||
try {
|
||||
t.click();
|
||||
} catch (__) {}
|
||||
}
|
||||
}, 180);
|
||||
}
|
||||
}, true);
|
||||
})();
|
||||
</script>
|
||||
<!-- Desktop dropdowns: robust open/close without body portal -->
|
||||
<script>
|
||||
(function() {
|
||||
if (!window.matchMedia('(min-width: 992px)').matches) return;
|
||||
var nav = document.getElementById('navbarManagement');
|
||||
if (!nav) return;
|
||||
|
||||
function findMenu(t) {
|
||||
if (!t) return null;
|
||||
var id = t.id;
|
||||
if (id) {
|
||||
var menuById = nav.querySelector('.dropdown-menu[aria-labelledby="' + id + '"]');
|
||||
if (menuById) return menuById;
|
||||
}
|
||||
try { return t.parentElement.querySelector(':scope > .dropdown-menu'); } catch (_) { return t.parentElement.querySelector('.dropdown-menu'); }
|
||||
}
|
||||
|
||||
function hideAll() {
|
||||
nav.querySelectorAll('.nav-item.dropend .dropdown-menu.show').forEach(function(m){ m.classList.remove('show'); m.style.visibility=''; m.style.opacity=''; });
|
||||
nav.querySelectorAll('.nav-item.dropend .dropdown-toggle[aria-expanded="true"]').forEach(function(t){ t.setAttribute('aria-expanded','false'); });
|
||||
}
|
||||
|
||||
function showDropend(t) {
|
||||
var menu = findMenu(t);
|
||||
if (!menu) return;
|
||||
try {
|
||||
new bootstrap.Dropdown(t, { autoClose: 'outside' }).show();
|
||||
} catch (_) {
|
||||
// Fallback without Bootstrap
|
||||
t.setAttribute('aria-expanded','true');
|
||||
menu.classList.add('show');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle dropdowns on click (prevent duplicate toggles)
|
||||
nav.addEventListener('click', function(e) {
|
||||
var t = e.target.closest('.nav-item.dropend > .dropdown-toggle');
|
||||
if (!t) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var menu = findMenu(t);
|
||||
var isOpen = !!(menu && menu.classList.contains('show'));
|
||||
hideAll();
|
||||
if (!isOpen) showDropend(t);
|
||||
}, true);
|
||||
|
||||
// Close when clicking outside the management sidebar
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!e.target.closest('#navbarManagement')) hideAll();
|
||||
}, true);
|
||||
// Close on ESC
|
||||
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') hideAll(); });
|
||||
})();
|
||||
</script>
|
||||
Reference in New Issue
Block a user