Implement modern sidebar navigation with hamburger menu

- Add responsive sidebar with hamburger toggle functionality
- Implement dual toggle buttons (inner/outer) for better UX
- Fix sidebar positioning to not overlap header and footer
- Add sticky footer with proper z-index layering
- Download and integrate Bootstrap source maps locally
- Optimize toggle icons: smaller, cleaner, no button styling
- Ensure sidebar respects footer boundaries
- Add smooth transitions and hover effects
- Fix active page highlighting and folder auto-expansion
- Create professional W3Schools-style navigation
- Maintain full offline capability with local assets
This commit is contained in:
Edwin Noorlander 2025-11-19 18:02:48 +01:00
parent 494ae7dc3b
commit 0f1c7234b8
10 changed files with 171 additions and 79 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -427,10 +427,16 @@ private function autoLinkPageTitles($content) {
if ($hasChildren) {
$folderId = 'folder-' . str_replace('/', '-', $item['path']);
$html .= '<span class="nav-link folder-toggle" data-bs-toggle="collapse" data-bs-target="#' . $folderId . '" aria-expanded="false">';
// Check if this folder contains the active page
$containsActive = $this->folderContainsActivePage($item['children']);
$ariaExpanded = $containsActive ? 'true' : 'false';
$collapseClass = $containsActive ? 'collapse show' : 'collapse';
$html .= '<span class="nav-link folder-toggle" data-bs-toggle="collapse" data-bs-target="#' . $folderId . '" aria-expanded="' . $ariaExpanded . '">';
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
$html .= '</span>';
$html .= '<ul class="nav flex-column ms-2 collapse" id="' . $folderId . '">';
$html .= '<ul class="nav flex-column ms-2 ' . $collapseClass . '" id="' . $folderId . '">';
$html .= $this->renderMenu($item['children'], $level + 1);
$html .= '</ul>';
} else {
@ -449,6 +455,21 @@ private function autoLinkPageTitles($content) {
}
return $html;
}
private function folderContainsActivePage($children) {
foreach ($children as $child) {
if ($child['type'] === 'folder') {
if (!empty($child['children']) && $this->folderContainsActivePage($child['children'])) {
return true;
}
} else {
if (isset($_GET['page']) && $_GET['page'] === $child['path']) {
return true;
}
}
}
return false;
}
}
$cms = new CodePressCMS($config);

View File

@ -4,9 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{page_title}} - {{site_title}}</title>
<link rel="icon" type="image/svg+xml" href="../engine/assets/favicon.svg">
<link href="../engine/assets/css/bootstrap.min.css" rel="stylesheet">
<link href="../engine/assets/css/bootstrap-icons.css" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="/engine/assets/favicon.svg">
<link href="/engine/assets/css/bootstrap.min.css" rel="stylesheet">
<link href="/engine/assets/css/bootstrap-icons.css" rel="stylesheet">
<style>
* {
margin: 0;
@ -22,12 +22,14 @@
display: flex;
flex-direction: column;
min-height: 100vh;
position: relative;
}
.main-wrapper {
flex: 1;
display: flex;
flex-direction: column;
min-height: calc(100vh - 70px); /* Minus header height */
}
.content-wrapper {
@ -43,34 +45,72 @@
overflow-y: auto;
flex-shrink: 0;
transition: transform 0.3s ease;
position: relative;
z-index: 1000;
position: fixed;
top: 70px;
left: 0;
height: calc(100vh - 140px); /* 70px header + 70px footer */
z-index: 999;
transform: translateX(0);
}
.sidebar.collapsed {
transform: translateX(-250px);
}
.sidebar-toggle {
position: fixed;
top: 80px;
left: 10px;
position: absolute;
top: 15px;
right: 15px;
z-index: 1001;
background-color: #0d6efd;
color: white;
background: none;
border: none;
border-radius: 5px;
padding: 8px 12px;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
transition: all 0.3s ease;
font-size: 20px;
color: #6c757d;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.sidebar-toggle:hover {
color: #0d6efd;
background-color: #f8f9fa;
}
.sidebar-toggle-inner {
/* Toggle inside sidebar */
}
.sidebar-toggle-outer {
position: fixed;
top: 90px;
left: 20px;
z-index: 1001;
background-color: white;
border: 1px solid #dee2e6;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.sidebar.collapsed .sidebar-toggle-inner {
right: auto;
left: 15px;
}
.sidebar.collapsed ~ .main-content .sidebar-toggle-outer {
display: block !important;
}
.sidebar:not(.collapsed) ~ .main-content .sidebar-toggle-outer {
display: none !important;
}
.sidebar-toggle:hover {
background-color: #0a58ca;
transform: scale(1.05);
}
.sidebar-toggle.shifted {
left: 270px;
.main-content {
flex: 1;
overflow-y: auto;
padding: 20px;
transition: all 0.3s ease;
margin-left: 250px;
}
.main-content.shifted {
.sidebar.collapsed ~ .main-content {
margin-left: 0;
}
@ -246,7 +286,7 @@
<div class="row align-items-center">
<div class="col">
<div class="d-flex align-items-center">
<img src="../engine/assets/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
<img src="/engine/assets/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
<h1 class="h3 mb-0">{{site_title}}</h1>
</div>
</div>
@ -262,10 +302,10 @@
<div class="main-wrapper">
<div class="content-wrapper">
<button class="sidebar-toggle" id="sidebarToggle">
<i class="bi bi-list"></i>
</button>
<nav class="sidebar" id="sidebar">
<div class="sidebar-toggle sidebar-toggle-inner" id="sidebarToggleInner">
<i class="bi bi-list"></i>
</div>
<div class="pt-3">
<ul class="nav flex-column">
{{menu}}
@ -274,6 +314,9 @@
</nav>
<main class="main-content">
<div class="sidebar-toggle sidebar-toggle-outer" id="sidebarToggleOuter" style="display: none;">
<i class="bi bi-list"></i>
</div>
<div>
{{breadcrumb}}
</div>
@ -287,7 +330,7 @@
</div>
</div>
<footer class="bg-light border-top py-3">
<footer class="bg-light border-top py-3" style="position: fixed; bottom: 0; left: 0; right: 0; z-index: 998;">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
@ -304,74 +347,90 @@
</div>
</footer>
<script src="../engine/assets/js/bootstrap.bundle.min.js"></script>
<script src="/engine/assets/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Sidebar toggle functionality
const sidebarToggle = document.getElementById('sidebarToggle');
const sidebarToggleInner = document.getElementById('sidebarToggleInner');
const sidebarToggleOuter = document.getElementById('sidebarToggleOuter');
const sidebar = document.getElementById('sidebar');
const mainContent = document.querySelector('.main-content');
sidebarToggle.addEventListener('click', function() {
// Initialize sidebar state (open by default)
sidebar.classList.remove('collapsed');
const innerIcon = sidebarToggleInner.querySelector('i');
const outerIcon = sidebarToggleOuter.querySelector('i');
innerIcon.classList.remove('bi-list');
innerIcon.classList.add('bi-x');
outerIcon.classList.remove('bi-list');
outerIcon.classList.add('bi-x');
function toggleSidebar() {
sidebar.classList.toggle('collapsed');
mainContent.classList.toggle('shifted');
sidebarToggle.classList.toggle('shifted');
// Change icon
const icon = this.querySelector('i');
// Change icons
if (sidebar.classList.contains('collapsed')) {
icon.classList.remove('bi-list');
icon.classList.add('bi-chevron-right');
innerIcon.classList.remove('bi-x');
innerIcon.classList.add('bi-list');
outerIcon.classList.remove('bi-x');
outerIcon.classList.add('bi-list');
} else {
icon.classList.remove('bi-chevron-right');
icon.classList.add('bi-list');
innerIcon.classList.remove('bi-list');
innerIcon.classList.add('bi-x');
outerIcon.classList.remove('bi-list');
outerIcon.classList.add('bi-x');
}
}
});
// Open folders that contain the current active page
const activeLink = document.querySelector('.nav-link.active');
if (activeLink) {
let parent = activeLink.closest('.collapse');
while (parent) {
const toggle = document.querySelector('[data-bs-target="#' + parent.id + '"]');
if (toggle) {
const collapse = new bootstrap.Collapse(parent, {
show: true
});
toggle.setAttribute('aria-expanded', 'true');
}
parent = parent.parentElement.closest('.collapse');
}
}
sidebarToggleInner.addEventListener('click', toggleSidebar);
sidebarToggleOuter.addEventListener('click', toggleSidebar);
// Folders are now automatically expanded by PHP if they contain the active page
// Close other folders when opening a new one
const folderToggles = document.querySelectorAll('.folder-toggle');
folderToggles.forEach(toggle => {
toggle.addEventListener('click', function(e) {
const targetId = this.getAttribute('data-bs-target');
const targetCollapse = document.querySelector(targetId);
const isExpanded = this.getAttribute('aria-expanded') === 'true';
if (!isExpanded) {
// Close all other folders
if (!isExpanded && targetCollapse) {
// Close all other folders first
folderToggles.forEach(otherToggle => {
if (otherToggle !== this) {
const otherTargetId = otherToggle.getAttribute('data-bs-target');
if (otherTargetId) {
const otherCollapse = document.querySelector(otherTargetId);
if (otherCollapse) {
const bsCollapse = bootstrap.Collapse.getInstance(otherCollapse);
if (bsCollapse) {
bsCollapse.hide();
} else {
new bootstrap.Collapse(otherCollapse, {
hide: true
});
}
otherCollapse.classList.remove('show');
otherToggle.setAttribute('aria-expanded', 'false');
// Reset arrow
const otherArrow = otherToggle.querySelector('.arrow');
if (otherArrow) {
otherArrow.style.transform = '';
}
}
}
}
});
// Open this folder
targetCollapse.classList.add('show');
this.setAttribute('aria-expanded', 'true');
// Rotate arrow
const arrow = this.querySelector('.arrow');
if (arrow) {
arrow.style.transform = 'rotate(90deg)';
}
} else if (isExpanded && targetCollapse) {
// Close this folder
targetCollapse.classList.remove('show');
this.setAttribute('aria-expanded', 'false');
// Reset arrow
const arrow = this.querySelector('.arrow');
if (arrow) {
arrow.style.transform = '';
}
}
});
});

1
public/engine Symbolic link
View File

@ -0,0 +1 @@
../engine

View File

@ -438,7 +438,7 @@ class CodePressCMS {
}
// Block direct access to content files
$requestUri = $_SERVER['REQUEST_URI'];
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
if (strpos($requestUri, '/content/') !== false) {
http_response_code(403);
echo '<h1>403 - Forbidden</h1><p>Direct access to content files is not allowed.</p>';

View File

@ -1,5 +1,5 @@
<?php
// Router file for PHP development server to handle security
// Router file for PHP development server to handle security and static files
$requestUri = $_SERVER['REQUEST_URI'];
$parsedUrl = parse_url($requestUri);
@ -23,9 +23,30 @@ foreach ($sensitiveFiles as $file) {
}
// Serve static files from engine/assets
if (strpos($path, '/engine/') === 0 && file_exists(__DIR__ . $path)) {
return false; // Let PHP server serve the file
if (strpos($path, '/engine/') === 0) {
$filePath = __DIR__ . $path;
if (file_exists($filePath)) {
// Set appropriate content type
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
$mimeTypes = [
'css' => 'text/css',
'js' => 'application/javascript',
'svg' => 'image/svg+xml',
'woff' => 'font/woff',
'woff2' => 'font/woff2',
'ttf' => 'font/ttf'
];
if (isset($mimeTypes[$extension])) {
header('Content-Type: ' . $mimeTypes[$extension]);
}
// Serve the file
readfile($filePath);
return true;
}
}
// Route all other requests to index.php
return false; // Let PHP server handle routing to index.php
include __DIR__ . '/index.php';
return true;

View File

@ -1,13 +1 @@
[Wed Nov 19 16:58:13 2025] PHP 8.4.14 Development Server (http://localhost:8080) started
[Wed Nov 19 16:58:22 2025] [::1]:43444 Accepted
[Wed Nov 19 16:58:22 2025] [::1]:43444 [200]: GET /
[Wed Nov 19 16:58:22 2025] [::1]:43444 Closing
[Wed Nov 19 16:58:26 2025] [::1]:59122 Accepted
[Wed Nov 19 16:58:26 2025] [::1]:59122 [200]: GET /content/home.md
[Wed Nov 19 16:58:26 2025] [::1]:59122 Closing
[Wed Nov 19 16:58:30 2025] [::1]:59138 Accepted
[Wed Nov 19 16:58:30 2025] [::1]:59138 [200]: GET /content/home.md
[Wed Nov 19 16:58:30 2025] [::1]:59138 Closing
[Wed Nov 19 16:59:06 2025] [::1]:38150 Accepted
[Wed Nov 19 16:59:06 2025] [::1]:38150 [200]: GET /content/home.md
[Wed Nov 19 16:59:06 2025] [::1]:38150 Closing
[Wed Nov 19 17:58:28 2025] Failed to listen on localhost:8080 (reason: Address already in use)