Refactor: Replace sidebar with horizontal navigation bar

- Remove sidebar and toggle functionality
- Add Bootstrap navbar with dropdown menus
- Move navigation to top between header and content
- Update menu rendering for Bootstrap dropdowns
- Clean up unused files (header.mustache, sidebar.mustache, sidebar.js)
- Add guide link with book icon in footer
- Simplify layout structure
- Remove duplicate code and fix syntax errors
- Add .gitignore for node_modules and other temp files
This commit is contained in:
2025-11-21 14:23:41 +01:00
parent 0f1c7234b8
commit a86809c243
87 changed files with 8369 additions and 1353 deletions

View File

@@ -2,6 +2,52 @@
require_once 'config.php';
// Simple template rendering without Mustache for now
class SimpleTemplate {
public static function render($template, $data) {
// Handle conditional blocks first
foreach ($data as $key => $value) {
if (is_array($value) || (is_string($value) && !empty($value))) {
// Handle {{#key}}...{{/key}} blocks
$pattern = '/{{#' . preg_quote($key, '/') . '}}(.*?){{\/' . preg_quote($key, '/') . '}}/s';
if (preg_match($pattern, $template, $matches)) {
$replacement = $matches[1];
$template = preg_replace($pattern, $replacement, $template);
}
// Handle {{^key}}...{{/key}} blocks (negative condition)
$pattern = '/{{\^' . preg_quote($key, '/') . '}}(.*?){{\/' . preg_quote($key, '/') . '}}/s';
$template = preg_replace($pattern, '', $template);
} else {
// Handle empty blocks
$pattern = '/{{#' . preg_quote($key, '/') . '}}.*?{{\/' . preg_quote($key, '/') . '}}/s';
$template = preg_replace($pattern, '', $template);
// Handle {{^key}}...{{/key}} blocks (show when empty)
$pattern = '/{{\^' . preg_quote($key, '/') . '}}(.*?){{\/' . preg_quote($key, '/') . '}}/s';
if (preg_match_all($pattern, $template, $matches)) {
foreach ($matches[1] as $match) {
$template = preg_replace('/{{\^' . preg_quote($key, '/') . '}}.*?{{\/' . preg_quote($key, '/') . '}}/s', $match, $template, 1);
}
}
}
}
// Handle variable replacements
foreach ($data as $key => $value) {
// Handle triple braces for unescaped HTML content
if (strpos($template, '{{{' . $key . '}}}') !== false) {
$template = str_replace('{{{' . $key . '}}}', $value, $template);
}
// Handle double braces for escaped content
elseif (strpos($template, '{{' . $key . '}}') !== false) {
$template = str_replace('{{' . $key . '}}', htmlspecialchars($value, ENT_QUOTES, 'UTF-8'), $template);
}
}
return $template;
}
}
$config = include 'config.php';
class CodePressCMS {
@@ -104,6 +150,16 @@ class CodePressCMS {
return $this->getSearchResults();
}
// Check if guide is requested
if (isset($_GET['guide'])) {
return $this->getGuidePage();
}
// Check if content directory is empty
if ($this->isContentDirEmpty()) {
return $this->getGuidePage();
}
$page = $_GET['page'] ?? $this->config['default_page'];
$page = preg_replace('/\.[^.]+$/', '', $page);
@@ -348,6 +404,46 @@ private function autoLinkPageTitles($content) {
];
}
private function isContentDirEmpty() {
$contentDir = $this->config['content_dir'];
if (!is_dir($contentDir)) {
return true;
}
$files = scandir($contentDir);
$files = array_diff($files, ['.', '..']);
return empty($files);
}
private function getGuidePage() {
$lang = $this->detectLanguage();
$guideFile = __DIR__ . '/../../guide/' . $lang . '.md';
if (!file_exists($guideFile)) {
$guideFile = __DIR__ . '/../../guide/en.md'; // Fallback to English
}
$content = file_get_contents($guideFile);
$result = $this->parseMarkdown($content);
// Set special title for guide
$result['title'] = 'Handleiding - CodePress CMS';
return $result;
}
private function detectLanguage() {
// Simple language detection based on browser Accept-Language header
$acceptLanguage = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
if (strpos($acceptLanguage, 'nl') !== false) {
return 'nl';
}
return 'en'; // Default to English
}
private function getError404() {
return [
'title' => 'Page Not Found',
@@ -364,27 +460,59 @@ private function autoLinkPageTitles($content) {
$menu = $this->getMenu();
$breadcrumb = $this->getBreadcrumb();
$template = file_get_contents($this->config['templates_dir'] . '/layout.html');
$template = str_replace('{{site_title}}', $this->config['site_title'], $template);
$template = str_replace('{{page_title}}', $page['title'], $template);
$template = str_replace('{{content}}', $page['content'], $template);
$template = str_replace('{{search_query}}', isset($_GET['search']) ? htmlspecialchars($_GET['search']) : '', $template);
$template = str_replace('{{breadcrumb}}', $breadcrumb, $template);
// Prepare template data
$templateData = [
'site_title' => $this->config['site_title'],
'page_title' => htmlspecialchars($page['title']),
'content' => $page['content'],
'search_query' => isset($_GET['search']) ? htmlspecialchars($_GET['search']) : '',
'menu' => $this->renderMenu($menu),
'breadcrumb' => $breadcrumb,
'default_page' => $this->config['default_page']
];
// File info for footer
$fileInfo = '';
if (isset($page['file_info'])) {
$fileInfo = '<i class="bi bi-file-text"></i> Created: ' . htmlspecialchars($page['file_info']['created']) .
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
$templateData['file_info'] = 'Created: ' . htmlspecialchars($page['file_info']['created']) .
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
} else {
$templateData['file_info'] = '';
}
$template = str_replace('{{file_info}}', $fileInfo, $template);
$menuHtml = $this->renderMenu($menu);
// Check if content exists for guide link
$hasContent = !$this->isContentDirEmpty();
$templateData['has_content'] = $hasContent;
$template = str_replace('{{menu}}', $menuHtml, $template);
// Don't show site title link on guide page
$templateData['show_site_link'] = !$this->isContentDirEmpty() && !isset($_GET['guide']);
echo $template;
// Load partials manually
$hasContent = !$this->isContentDirEmpty() && !isset($_GET['guide']);
$headerContent = file_get_contents($this->config['templates_dir'] . '/assets/header.mustache');
if (!$hasContent) {
// Remove the link from header when no content
$headerContent = preg_replace('/<a href="[^"]*" class="site-title-link">\s*<h1[^>]*>(.*?)<\/h1>\s*<\/a>/', '<h1 class="h3 mb-0">$1</h1>', $headerContent);
}
$footerContent = file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache');
if (!$hasContent) {
// Remove guide link from footer when no content
$footerContent = preg_replace('/<span class="file-details">\s*\|\s*<a href="\?guide"[^>]*>Handleiding<\/a><\/span>/', '', $footerContent);
}
$partials = [
'navigation' => file_get_contents($this->config['templates_dir'] . '/assets/navigation.mustache'),
'footer' => file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache')
];
// Replace partials in template
$template = file_get_contents($this->config['templates_dir'] . '/layout.mustache');
$template = str_replace('{{>navigation}}', $partials['navigation'], $template);
$template = str_replace('{{>footer}}', $partials['footer'], $template);
// Render template with data
echo SimpleTemplate::render($template, $templateData);
}
private function getBreadcrumb() {
@@ -423,33 +551,27 @@ private function autoLinkPageTitles($content) {
foreach ($items as $item) {
if ($item['type'] === 'folder') {
$hasChildren = !empty($item['children']);
$html .= '<li class="nav-item">';
if ($hasChildren) {
$folderId = 'folder-' . str_replace('/', '-', $item['path']);
// 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 ' . $collapseClass . '" id="' . $folderId . '">';
$isExpanded = $this->folderContainsActivePage($item['children']);
$html .= '<li class="nav-item dropdown">';
$html .= '<a class="nav-link dropdown-toggle" href="#" id="' . $folderId . '" role="button" data-bs-toggle="dropdown" aria-expanded="' . ($isExpanded ? 'true' : 'false') . '">';
$html .= htmlspecialchars($item['title']);
$html .= '</a>';
$html .= '<ul class="dropdown-menu" aria-labelledby="' . $folderId . '">';
$html .= $this->renderMenu($item['children'], $level + 1);
$html .= '</ul>';
$html .= '</li>';
} else {
$html .= '<span class="nav-link folder-disabled" disabled>';
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
$html .= '</span>';
$html .= '<li class="nav-item">';
$html .= '<span class="nav-link text-muted">' . htmlspecialchars($item['title']) . '</span>';
$html .= '</li>';
}
$html .= '</li>';
} else {
$active = (isset($_GET['page']) && $_GET['page'] === $item['path']) ? 'active' : '';
$html .= '<li class="nav-item">';
$html .= '<a class="nav-link page-link ' . $active . '" href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['title']) . '</a>';
$html .= '<a class="nav-link ' . $active . '" href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['title']) . '</a>';
$html .= '</li>';
}
}
@@ -470,7 +592,4 @@ private function autoLinkPageTitles($content) {
}
return false;
}
}
$cms = new CodePressCMS($config);
$cms->render();
}