## Complete Multi-language System & Navigation Enhancement

### Features Added:
- **Multi-language Support**: Dutch/English with URL-based switching (?lang=nl|en)
- **Theme Customization**: Configurable header/navigation colors via config.json
- **Navigation Improvements**: Active states, dropdown chevron icons, visual distinction
- **Mobile Responsive**: Separate desktop/mobile search layouts
- **Template System**: Fixed rendering pipeline for all partials

### Technical Fixes:
- Fixed language file path (engine/lang/ vs engine/core/class/../lang/)
- Added template data rendering to layout template
- Implemented navigation active state for default/home page
- Added chevron icons to dropdown folders for visual distinction
- Removed hardcoded navigation opacity class for theme colors

### Files Modified:
- config.json: Added theme and language configuration
- engine/core/class/CodePressCMS.php: Multi-language and navigation logic
- engine/templates/: Enhanced header, footer, navigation, layout
- engine/lang/: Dutch and English translation files
- public/assets/css/mobile.css: Mobile responsive fixes

### Result:
Fully functional multi-language CMS with proper navigation states and theme customization.
This commit is contained in:
Edwin Noorlander 2025-11-22 15:29:47 +01:00
parent 863661612a
commit a2b7fcb1a8
9 changed files with 431 additions and 121 deletions

View File

@ -1,12 +1,18 @@
{
"site_title": "CodePress",
"content_dir": "public/content",
"content_dir": "content",
"templates_dir": "engine/templates",
"default_page": "welkom",
"theme": {
"primary_color": "#0d6efd",
"navbar_style": "bg-primary"
"header_color": "#0a369d",
"header_font_color": "#ffffff",
"navigation_color": "#2754b4",
"navigation_font_color": "#ffffff"
},
"language": {
"default": "nl",
"available": ["nl", "en"]
},
"seo": {
"description": "CodePress CMS - Lightweight file-based content management system",
@ -22,4 +28,4 @@
"search_enabled": true,
"breadcrumbs_enabled": true
}
}
}

View File

@ -24,6 +24,8 @@ class CodePressCMS {
private $config;
private $menu = [];
private $searchResults = [];
private $currentLanguage;
private $translations = [];
/**
* Constructor - Initialize the CMS with configuration
@ -32,6 +34,8 @@ class CodePressCMS {
*/
public function __construct($config) {
$this->config = $config;
$this->currentLanguage = $this->getCurrentLanguage();
$this->translations = $this->loadTranslations($this->currentLanguage);
$this->buildMenu();
if (isset($_GET['search'])) {
@ -39,6 +43,46 @@ class CodePressCMS {
}
}
/**
* Get current language from URL or use default
*
* @return string Current language code
*/
private function getCurrentLanguage() {
return $_GET['lang'] ?? $this->config['language']['default'] ?? 'nl';
}
/**
* Load translations for specified language
*
* @param string $lang Language code
* @return array Translations array
*/
private function loadTranslations($lang) {
$langFile = __DIR__ . '/../../lang/' . $lang . '.php';
error_log("Loading language file: " . $langFile);
if (file_exists($langFile)) {
$translations = include $langFile;
error_log("Loaded translations for " . $lang . ": " . print_r($translations, true));
return $translations;
}
// Fallback to default language
$defaultLang = $this->config['language']['default'] ?? 'nl';
$defaultLangFile = __DIR__ . '/../../lang/' . $defaultLang . '.php';
error_log("Fallback to default language: " . $defaultLangFile);
return file_exists($defaultLangFile) ? include $defaultLangFile : [];
}
/**
* Get translated text
*
* @param string $key Translation key
* @return string Translated text
*/
public function t($key) {
return $this->translations[$key] ?? $key;
}
/**
* Build menu structure from content directory
*
@ -263,12 +307,12 @@ class CodePressCMS {
*/
private function getSearchResults() {
$query = $_GET['search'];
$content = '<h2>Search Results for: "' . htmlspecialchars($query) . '"</h2>';
$content = '<h2>' . $this->t('search') . ' ' . $this->t('results_found') . ': "' . htmlspecialchars($query) . '"</h2>';
if (empty($this->searchResults)) {
$content .= '<p>No results found.</p>';
$content .= '<p>' . $this->t('no_results') . '.</p>';
} else {
$content .= '<p>Found ' . count($this->searchResults) . ' results:</p>';
$content .= '<p>' . count($this->searchResults) . ' ' . $this->t('results_found') . ':</p>';
foreach ($this->searchResults as $result) {
$content .= '<div class="card mb-3">';
$content .= '<div class="card-body">';
@ -593,7 +637,7 @@ class CodePressCMS {
$result = $this->parseMarkdown($content);
// Set special title for guide
$result['title'] = 'Handleiding - CodePress CMS';
$result['title'] = $this->t('manual') . ' - CodePress CMS';
return $result;
}
@ -647,11 +691,8 @@ class CodePressCMS {
sort($items);
$hasContent = false;
$content .= '<div class="row">';
// Subdirectories
$subdirs = [];
$files = [];
// Collect all items
$allItems = [];
foreach ($items as $item) {
if ($item[0] === '.') continue;
@ -660,57 +701,46 @@ class CodePressCMS {
$relativePath = $pagePath ? $pagePath . '/' . $item : $item;
if (is_dir($itemPath)) {
$subdirs[] = [
$allItems[] = [
'name' => ucfirst($item),
'path' => $relativePath,
'url' => '?page=' . $relativePath
'url' => '?page=' . $relativePath,
'icon' => 'bi-folder',
'type' => 'directory'
];
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
$extractedTitle = $this->extractPageTitle($itemPath);
$fileTitle = $extractedTitle ?: ucfirst(pathinfo($item, PATHINFO_FILENAME));
$pathWithoutExt = preg_replace('/\.[^.]+$/', '', $relativePath);
$files[] = [
$icon = pathinfo($item, PATHINFO_EXTENSION) === 'md' ? 'bi-file-text' :
(pathinfo($item, PATHINFO_EXTENSION) === 'php' ? 'bi-file-code' : 'bi-file-earmark');
$allItems[] = [
'name' => $fileTitle,
'path' => $pathWithoutExt,
'url' => '?page=' . $pathWithoutExt,
'type' => pathinfo($item, PATHINFO_EXTENSION)
'icon' => $icon,
'type' => 'file'
];
}
}
// Display subdirectories
if (!empty($subdirs)) {
$content .= '<div class="col-md-6">';
$content .= '<h3>📁 Mappen</h3>';
// Display all items in a single column
if (!empty($allItems)) {
$content .= '<div class="list-group">';
foreach ($subdirs as $subdir) {
$content .= '<a href="' . htmlspecialchars($subdir['url']) . '" class="list-group-item list-group-item-action">';
$content .= '<i class="bi bi-folder"></i> ' . htmlspecialchars($subdir['name']);
foreach ($allItems as $item) {
$content .= '<a href="' . htmlspecialchars($item['url']) . '" class="list-group-item list-group-item-action d-flex align-items-center">';
$content .= '<i class="bi ' . $item['icon'] . ' me-3"></i>';
$content .= '<span>' . htmlspecialchars($item['name']) . '</span>';
$content .= '</a>';
}
$content .= '</div></div>';
$hasContent = true;
}
// Display files
if (!empty($files)) {
$content .= '<div class="col-md-6">';
$content .= '<h3>📄 Pagina\'s</h3>';
$content .= '<div class="list-group">';
foreach ($files as $file) {
$icon = $file['type'] === 'md' ? 'bi-file-text' : ($file['type'] === 'php' ? 'bi-file-code' : 'bi-file-earmark');
$content .= '<a href="' . htmlspecialchars($file['url']) . '" class="list-group-item list-group-item-action">';
$content .= '<i class="bi ' . $icon . '"></i> ' . htmlspecialchars($file['name']);
$content .= '</a>';
}
$content .= '</div></div>';
$content .= '</div>';
$hasContent = true;
}
$content .= '</div>';
if (!$hasContent) {
$content .= '<p>Deze map is leeg.</p>';
$content .= '<p>' . $this->t('directory_empty') . '.</p>';
}
return [
@ -726,8 +756,8 @@ class CodePressCMS {
*/
private function getError404() {
return [
'title' => 'Page Not Found',
'content' => '<h1>404 - Page Not Found</h1><p>The page you are looking for does not exist.</p>'
'title' => $this->t('page_not_found'),
'content' => '<h1>404 - ' . $this->t('page_not_found') . '</h1><p>' . $this->t('page_not_found_text') . '</p>'
];
}
@ -748,7 +778,7 @@ class CodePressCMS {
public function render() {
$page = $this->getPage();
$menu = $this->getMenu();
$breadcrumb = $this->getBreadcrumb();
$breadcrumb = $this->generateBreadcrumb();
// Get homepage title
$homepageTitle = $this->getHomepageTitle();
@ -764,30 +794,57 @@ class CodePressCMS {
'default_page' => $this->config['default_page'],
'homepage' => $this->config['default_page'],
'homepage_title' => $homepageTitle,
'is_homepage' => (!isset($_GET['page']) || $_GET['page'] === $this->config['default_page']),
'home_active_class' => (!isset($_GET['page']) || $_GET['page'] === $this->config['default_page']) ? 'active' : '',
'author_name' => $this->config['author']['name'] ?? 'CodePress Developer',
'author_website' => $this->config['author']['website'] ?? '#',
'author_git' => $this->config['author']['git'] ?? '#',
'seo_description' => $this->config['seo']['description'] ?? 'CodePress CMS - Lightweight file-based content management system',
'seo_keywords' => $this->config['seo']['keywords'] ?? 'cms, php, content management, file-based'
'seo_keywords' => $this->config['seo']['keywords'] ?? 'cms, php, content management, file-based',
// Theme colors
'header_color' => $this->config['theme']['header_color'] ?? '#0d6efd',
'header_font_color' => $this->config['theme']['header_font_color'] ?? '#ffffff',
'navigation_color' => $this->config['theme']['navigation_color'] ?? '#f8f9fa',
'navigation_font_color' => $this->config['theme']['navigation_font_color'] ?? '#000000',
// Language
'current_lang' => $this->currentLanguage,
'current_lang_upper' => strtoupper($this->currentLanguage),
'available_langs' => $this->config['language']['available'] ?? ['nl', 'en'],
// Translations
't_home' => $this->t('home'),
't_search' => $this->t('search'),
't_search_placeholder' => $this->t('search_placeholder'),
't_search_button' => $this->t('search_button'),
't_welcome' => $this->t('welcome'),
't_created' => $this->t('created'),
't_modified' => $this->t('modified'),
't_author' => $this->t('author'),
't_manual' => $this->t('manual'),
't_no_content' => $this->t('no_content'),
't_no_results' => $this->t('no_results'),
't_results_found' => $this->t('results_found'),
't_breadcrumb_home' => $this->t('breadcrumb_home'),
't_file_details' => $this->t('file_details'),
't_guide' => $this->t('guide'),
't_powered_by' => $this->t('powered_by'),
't_directory_empty' => $this->t('directory_empty'),
't_page_not_found' => $this->t('page_not_found'),
't_page_not_found_text' => $this->t('page_not_found_text'),
't_mappen' => $this->t('mappen'),
't_paginas' => $this->t('paginas')
];
// File info for footer
if (isset($page['file_info'])) {
$templateData['file_info'] = 'Created: ' . htmlspecialchars($page['file_info']['created']) .
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
$templateData['file_info'] = $this->t('created') . ': ' . htmlspecialchars($page['file_info']['created']) .
' | ' . $this->t('modified') . ': ' . htmlspecialchars($page['file_info']['modified']);
$templateData['file_info_block'] = '<span class="file-details"> | ' . $templateData['file_info'] . '</span>';
} else {
$templateData['file_info'] = '';
$templateData['file_info_block'] = '';
}
// File info for footer
if (isset($page['file_info'])) {
$templateData['file_info'] = 'Created: ' . htmlspecialchars($page['file_info']['created']) .
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
} else {
$templateData['file_info'] = '';
}
// Check if content exists for guide link
$hasContent = !$this->isContentDirEmpty();
@ -796,60 +853,35 @@ class CodePressCMS {
// Don't show site title link on guide page
$templateData['show_site_link'] = !$this->isContentDirEmpty() && !isset($_GET['guide']);
// 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);
}
// Load and render all templates with data
$layoutTemplate = file_get_contents($this->config['templates_dir'] . '/layout.mustache');
$headerTemplate = file_get_contents($this->config['templates_dir'] . '/assets/header.mustache');
$navigationTemplate = file_get_contents($this->config['templates_dir'] . '/assets/navigation.mustache');
$footerTemplate = file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache');
// Determine content type and load appropriate template
$contentType = $this->getContentType($page);
$contentTemplateFile = $this->config['templates_dir'] . '/' . $contentType . '_content.mustache';
$contentTemplate = file_exists($contentTemplateFile) ? file_get_contents($contentTemplateFile) : '<div class="content">{{{content}}}</div>';
// Determine content type and load appropriate template
$pagePath = $_GET['page'] ?? $this->config['default_page'];
$pagePath = preg_replace('/\.[^.]+$/', '', $pagePath);
$filePath = $this->config['content_dir'] . '/' . $pagePath;
$contentType = 'markdown'; // default
if (file_exists($filePath . '.md')) {
$contentType = 'markdown';
} elseif (file_exists($filePath . '.php')) {
$contentType = 'php';
} elseif (file_exists($filePath . '.html')) {
$contentType = 'html';
}
// Render all templates with data
$renderedHeader = SimpleTemplate::render($headerTemplate, $templateData);
$renderedNavigation = SimpleTemplate::render($navigationTemplate, $templateData);
$renderedFooter = SimpleTemplate::render($footerTemplate, $templateData);
$renderedContent = SimpleTemplate::render($contentTemplate, $templateData);
$contentTemplateFile = $this->config['templates_dir'] . '/' . $contentType . '_content.mustache';
$contentTemplate = file_exists($contentTemplateFile) ? file_get_contents($contentTemplateFile) : '<div class="content">{{{content}}}</div>';
// Replace partials in layout
$finalTemplate = str_replace('{{>header}}', $renderedHeader, $layoutTemplate);
$finalTemplate = str_replace('{{>navigation}}', $renderedNavigation, $finalTemplate);
$finalTemplate = str_replace('{{>footer}}', $renderedFooter, $finalTemplate);
$finalTemplate = str_replace('{{>content_template}}', $renderedContent, $finalTemplate);
$partials = [
'header' => file_get_contents($this->config['templates_dir'] . '/assets/header.mustache'),
'navigation' => file_get_contents($this->config['templates_dir'] . '/assets/navigation.mustache'),
'footer' => file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache'),
'content_template' => $contentTemplate
];
// Render the final layout with all template data
$renderedLayout = SimpleTemplate::render($finalTemplate, $templateData);
// Replace partials in template
$template = file_get_contents($this->config['templates_dir'] . '/layout.mustache');
$template = str_replace('{{>header}}', $partials['header'], $template);
$template = str_replace('{{>navigation}}', $partials['navigation'], $template);
$template = str_replace('{{>footer}}', $partials['footer'], $template);
$template = str_replace('{{>content_template}}', $partials['content_template'], $template);
// Render template with data
$renderedTemplate = SimpleTemplate::render($template, $templateData);
echo $renderedTemplate;
echo $renderedLayout;
}
/**
@ -857,30 +889,37 @@ class CodePressCMS {
*
* @return string Breadcrumb HTML
*/
private function getBreadcrumb() {
private function generateBreadcrumb() {
if (isset($_GET['search'])) {
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">Home</a></li><li class="breadcrumb-item active">Search</li></ol></nav>';
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">></a></li><li class="breadcrumb-item"> > </li><li class="breadcrumb-item active">' . $this->t('search') . '</li></ol></nav>';
}
$page = $_GET['page'] ?? $this->config['default_page'];
$page = preg_replace('/\.[^.]+$/', '', $page);
if ($page === $this->config['default_page']) {
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item active">Home</li></ol></nav>';
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item active"><i class="bi bi-house"></i></li></ol></nav>';
}
$parts = explode('/', $page);
$breadcrumb = '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">Home</a></li>';
$breadcrumb = '<nav aria-label="breadcrumb"><ol class="breadcrumb">';
// Start with home icon linking to default page (root)
$breadcrumb .= '<li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '"><i class="bi bi-house"></i></a></li>';
// Split page path and build breadcrumb items
$parts = explode('/', $page);
$currentPath = '';
$path = '';
foreach ($parts as $i => $part) {
$path .= ($path ? '/' : '') . $part;
$currentPath .= ($currentPath ? '/' : '') . $part;
$title = ucfirst($part);
if ($i === count($parts) - 1) {
$breadcrumb .= '<li class="breadcrumb-item active">' . $title . '</li>';
// Last part - active page
$breadcrumb .= '<li class="breadcrumb-item"> > </li><li class="breadcrumb-item active">' . $title . '</li>';
} else {
$breadcrumb .= '<li class="breadcrumb-item"><a href="?page=' . $path . '">' . $title . '</a></li>';
// Parent directory - clickable link with separator
$breadcrumb .= '<li class="breadcrumb-item"> > </li><li class="breadcrumb-item"><a href="?page=' . $currentPath . '">' . $title . '</a></li>';
}
}
@ -909,7 +948,7 @@ class CodePressCMS {
// Root level folders
$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 .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-down"></i>';
$html .= '</a>';
$html .= '<ul class="dropdown-menu" aria-labelledby="' . $folderId . '">';
$html .= $this->renderMenu($item['children'], $level + 1);
@ -919,7 +958,7 @@ class CodePressCMS {
// Nested folders in dropdown
$html .= '<li class="dropdown-submenu">';
$html .= '<a class="dropdown-item dropdown-toggle" href="#" id="' . $folderId . '">';
$html .= htmlspecialchars($item['title']);
$html .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-down"></i>';
$html .= '</a>';
$html .= '<ul class="dropdown-menu" aria-labelledby="' . $folderId . '">';
$html .= $this->renderMenu($item['children'], $level + 1);

26
engine/lang/en.php Normal file
View File

@ -0,0 +1,26 @@
<?php
return [
'site_title' => 'CodePress',
'home' => 'Home',
'search' => 'Search',
'search_placeholder' => 'Search...',
'search_button' => 'Search',
'welcome' => 'Welcome',
'created' => 'Created',
'modified' => 'Modified',
'author' => 'Author',
'manual' => 'Manual',
'no_content' => 'No content found',
'no_results' => 'No results found',
'results_found' => 'results found',
'breadcrumb_home' => 'Home',
'file_details' => 'File details',
'guide' => 'Guide',
'powered_by' => 'Powered by',
't_powered_by' => 'Powered by',
'directory_empty' => 'This directory is empty',
'page_not_found' => 'Page Not Found',
'page_not_found_text' => 'The page you are looking for does not exist.',
'mappen' => 'Folders',
'paginas' => 'Pages'
];

26
engine/lang/nl.php Normal file
View File

@ -0,0 +1,26 @@
<?php
return [
'site_title' => 'CodePress',
'home' => 'Home',
'search' => 'Zoeken',
'search_placeholder' => 'Zoeken...',
'search_button' => 'Zoeken',
'welcome' => 'Welkom',
'created' => 'Aangemaakt',
'modified' => 'Aangepast',
'author' => 'Auteur',
'manual' => 'Handleiding',
'no_content' => 'Geen inhoud gevonden',
'no_results' => 'Geen resultaten gevonden',
'results_found' => 'resultaten gevonden',
'breadcrumb_home' => 'Home',
'file_details' => 'Bestandsdetails',
'guide' => 'Handleiding',
'powered_by' => 'Mogelijk gemaakt door',
't_powered_by' => 'Mogelijk gemaakt door',
'directory_empty' => 'Deze map is leeg',
'page_not_found' => 'Pagina niet gevonden',
'page_not_found_text' => 'De pagina die u zoekt bestaat niet.',
'mappen' => 'Mappen',
'paginas' => 'Pagina\'s'
];

View File

@ -10,11 +10,11 @@
</div>
<div class="site-info">
<small class="text-muted">
<a href="?guide" class="guide-link" title="Handleiding">
<a href="?guide" class="guide-link" title="{{t_guide}}">
<i class="bi bi-book"></i>
</a>
<span class="ms-2">|</span>
Powered by <a href="https://git.noorlander.info/E.Noorlander/CodePress.git" target="_blank" rel="noopener">CodePress CMS</a>
{{t_powered_by}} <a href="https://git.noorlander.info/E.Noorlander/CodePress.git" target="_blank" rel="noopener">CodePress CMS</a>
</small>
</div>
</div>

View File

@ -1,18 +1,50 @@
<header class="navbar navbar-expand-lg navbar-dark bg-primary">
<header class="navbar navbar-expand-lg navbar-dark" style="background-color: var(--header-bg);">
<div class="container-fluid">
<a class="navbar-brand" href="?page={{default_page}}">
<img src="/assets/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
{{site_title}}
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#searchBar" aria-controls="searchBar" aria-expanded="false" aria-label="Toggle search">
<i class="bi bi-search"></i>
</button>
<!-- Desktop search and language -->
<div class="d-none d-lg-flex ms-auto align-items-center">
<form class="d-flex me-3" method="GET" action="">
<input class="form-control me-2" type="search" name="search" placeholder="{{t_search_placeholder}}" value="{{search_query}}">
<button class="btn btn-outline-light" type="submit">{{t_search_button}}</button>
</form>
<!-- Language switcher -->
<div class="dropdown">
<button class="btn btn-outline-light" type="button" data-bs-toggle="dropdown">
{{current_lang_upper}} <i class="bi bi-chevron-down"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="?lang=nl&page={{homepage}}">NL</a></li>
<li><a class="dropdown-item" href="?lang=en&page={{homepage}}">EN</a></li>
</ul>
</div>
</div>
<div class="collapse navbar-collapse" id="searchBar">
<form class="d-flex ms-auto" method="GET" action="">
<input class="form-control me-2" type="search" name="search" placeholder="Search..." value="{{search_query}}">
<button class="btn btn-outline-light" type="submit">Search</button>
<!-- Mobile search and language toggle -->
<div class="d-lg-none">
<button class="btn btn-outline-light" type="button" data-bs-toggle="collapse" data-bs-target="#mobileSearch" aria-controls="mobileSearch" aria-expanded="false" aria-label="Toggle search">
<i class="bi bi-search"></i>
</button>
<button class="btn btn-outline-light" type="button" data-bs-toggle="dropdown">
{{current_lang_upper}} <i class="bi bi-chevron-down"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="?lang=nl&page={{homepage}}">NL</a></li>
<li><a class="dropdown-item" href="?lang=en&page={{homepage}}">EN</a></li>
</ul>
</div>
</div>
<!-- Mobile search bar -->
<div class="collapse navbar-collapse d-lg-none" id="mobileSearch">
<div class="container-fluid px-0">
<form class="d-flex px-3 pb-3" method="GET" action="">
<input class="form-control me-2" type="search" name="search" placeholder="{{t_search_placeholder}}" value="{{search_query}}">
<button class="btn btn-outline-light" type="submit">{{t_search_button}}</button>
</form>
</div>
</div>

View File

@ -1,10 +1,10 @@
<nav class="navigation-section border-bottom navigation-50-opacity">
<nav class="navigation-section">
<div class="container-fluid">
<div class="row align-items-center">
<div class="col">
<ul class="nav nav-tabs flex-wrap">
<li class="nav-item">
<a class="nav-link" href="?page={{homepage}}">
<a class="nav-link {{home_active_class}}" href="?page={{homepage}}">
<i class="bi bi-house"></i> {{homepage_title}}
</a>
</li>

View File

@ -25,6 +25,133 @@
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
<link href="/assets/css/bootstrap-icons.css" rel="stylesheet">
<link href="/assets/css/style.css" rel="stylesheet">
<link href="/assets/css/mobile.css" rel="stylesheet">
<!-- Dynamic theme colors -->
<style>
:root {
--header-bg: {{header_color}};
--header-font: {{header_font_color}};
--nav-bg: {{navigation_color}};
--nav-font: {{navigation_font_color}};
}
/* Header styles */
.navbar {
background-color: var(--header-bg) !important;
}
.navbar .navbar-brand,
.navbar .navbar-text,
.navbar .form-control,
.navbar .btn {
color: var(--header-font) !important;
}
.navbar .form-control::placeholder {
color: rgba(255,255,255,0.7) !important;
}
.navbar .btn-outline-light {
border-color: var(--header-font) !important;
}
/* Language dropdown styling */
.dropdown-menu {
background-color: var(--header-bg) !important;
border: 1px solid var(--header-font) !important;
}
.dropdown-item {
color: var(--header-font) !important;
}
.dropdown-item:hover {
background-color: rgba(255,255,255,0.1) !important;
color: var(--header-font) !important;
}
/* Hide Bootstrap dropdown arrow and use custom icon */
.dropdown-toggle::after {
display: none !important;
}
.btn-outline-light {
color: var(--header-font) !important;
border-color: var(--header-font) !important;
}
.btn-outline-light:hover {
background-color: rgba(255,255,255,0.1) !important;
color: var(--header-font) !important;
}
/* Fix button color when dropdown is open */
.btn-outline-light:focus,
.btn-outline-light:active,
.show > .btn-outline-light.dropdown-toggle {
background-color: rgba(255,255,255,0.1) !important;
color: var(--header-font) !important;
border-color: var(--header-font) !important;
box-shadow: none !important;
}
.bi-chevron-down {
font-size: 0.75em;
margin-left: 0.25rem;
}
/* Remove Bootstrap default breadcrumb separators */
.breadcrumb-item + .breadcrumb-item::before {
content: "" !important;
padding: 0 !important;
}
/* Custom breadcrumb styling */
.breadcrumb {
--bs-breadcrumb-divider: "";
}
.breadcrumb-item {
color: var(--nav-font) !important;
}
.breadcrumb-item a {
color: var(--nav-font) !important;
text-decoration: none;
}
.breadcrumb-item a:hover {
text-decoration: underline;
}
/* Navigation section background */
.navigation-section {
background-color: var(--nav-bg) !important;
color: var(--nav-font) !important;
}
/* Remove nav-tabs background so it inherits from parent */
.nav-tabs {
background-color: transparent !important;
border: none !important;
}
.nav-tabs .nav-link {
background-color: transparent !important;
border: none !important;
color: var(--nav-font) !important;
}
.nav-tabs .nav-link:hover {
background-color: rgba(255,255,255,0.1) !important;
}
.nav-tabs .nav-link.active {
background-color: rgba(255,255,255,0.2) !important;
border-bottom: 2px solid var(--nav-font) !important;
}
</style>
</head>
<body>
{{>header}}

View File

@ -0,0 +1,54 @@
/* Mobile search improvements */
@media (max-width: 991.98px) {
.navbar {
position: relative;
}
/* Hide desktop elements on mobile */
.d-none.d-lg-flex {
display: none !important;
}
#mobileSearch {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--header-bg);
z-index: 1030;
}
#mobileSearch.show {
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
/* Ensure navigation doesn't overlap with expanded search */
.navigation-section {
margin-top: 0;
border: none !important;
}
}
/* Desktop improvements */
@media (min-width: 992px) {
/* Hide mobile elements on desktop */
.d-lg-none {
display: none !important;
}
/* Hide mobile search on desktop */
#mobileSearch {
display: none !important;
}
}
/* Search form improvements */
.form-control[type="search"] {
min-width: 200px;
}
@media (max-width: 576px) {
.form-control[type="search"] {
min-width: 150px;
}
}