## 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:
parent
863661612a
commit
a2b7fcb1a8
12
config.json
12
config.json
@ -1,12 +1,18 @@
|
|||||||
{
|
{
|
||||||
"site_title": "CodePress",
|
"site_title": "CodePress",
|
||||||
"content_dir": "public/content",
|
"content_dir": "content",
|
||||||
"templates_dir": "engine/templates",
|
"templates_dir": "engine/templates",
|
||||||
"default_page": "welkom",
|
"default_page": "welkom",
|
||||||
|
|
||||||
"theme": {
|
"theme": {
|
||||||
"primary_color": "#0d6efd",
|
"header_color": "#0a369d",
|
||||||
"navbar_style": "bg-primary"
|
"header_font_color": "#ffffff",
|
||||||
|
"navigation_color": "#2754b4",
|
||||||
|
"navigation_font_color": "#ffffff"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"default": "nl",
|
||||||
|
"available": ["nl", "en"]
|
||||||
},
|
},
|
||||||
"seo": {
|
"seo": {
|
||||||
"description": "CodePress CMS - Lightweight file-based content management system",
|
"description": "CodePress CMS - Lightweight file-based content management system",
|
||||||
|
|||||||
@ -24,6 +24,8 @@ class CodePressCMS {
|
|||||||
private $config;
|
private $config;
|
||||||
private $menu = [];
|
private $menu = [];
|
||||||
private $searchResults = [];
|
private $searchResults = [];
|
||||||
|
private $currentLanguage;
|
||||||
|
private $translations = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor - Initialize the CMS with configuration
|
* Constructor - Initialize the CMS with configuration
|
||||||
@ -32,6 +34,8 @@ class CodePressCMS {
|
|||||||
*/
|
*/
|
||||||
public function __construct($config) {
|
public function __construct($config) {
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
|
$this->currentLanguage = $this->getCurrentLanguage();
|
||||||
|
$this->translations = $this->loadTranslations($this->currentLanguage);
|
||||||
$this->buildMenu();
|
$this->buildMenu();
|
||||||
|
|
||||||
if (isset($_GET['search'])) {
|
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
|
* Build menu structure from content directory
|
||||||
*
|
*
|
||||||
@ -263,12 +307,12 @@ class CodePressCMS {
|
|||||||
*/
|
*/
|
||||||
private function getSearchResults() {
|
private function getSearchResults() {
|
||||||
$query = $_GET['search'];
|
$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)) {
|
if (empty($this->searchResults)) {
|
||||||
$content .= '<p>No results found.</p>';
|
$content .= '<p>' . $this->t('no_results') . '.</p>';
|
||||||
} else {
|
} else {
|
||||||
$content .= '<p>Found ' . count($this->searchResults) . ' results:</p>';
|
$content .= '<p>' . count($this->searchResults) . ' ' . $this->t('results_found') . ':</p>';
|
||||||
foreach ($this->searchResults as $result) {
|
foreach ($this->searchResults as $result) {
|
||||||
$content .= '<div class="card mb-3">';
|
$content .= '<div class="card mb-3">';
|
||||||
$content .= '<div class="card-body">';
|
$content .= '<div class="card-body">';
|
||||||
@ -593,7 +637,7 @@ class CodePressCMS {
|
|||||||
$result = $this->parseMarkdown($content);
|
$result = $this->parseMarkdown($content);
|
||||||
|
|
||||||
// Set special title for guide
|
// Set special title for guide
|
||||||
$result['title'] = 'Handleiding - CodePress CMS';
|
$result['title'] = $this->t('manual') . ' - CodePress CMS';
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
@ -647,11 +691,8 @@ class CodePressCMS {
|
|||||||
sort($items);
|
sort($items);
|
||||||
$hasContent = false;
|
$hasContent = false;
|
||||||
|
|
||||||
$content .= '<div class="row">';
|
// Collect all items
|
||||||
|
$allItems = [];
|
||||||
// Subdirectories
|
|
||||||
$subdirs = [];
|
|
||||||
$files = [];
|
|
||||||
|
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
if ($item[0] === '.') continue;
|
if ($item[0] === '.') continue;
|
||||||
@ -660,57 +701,46 @@ class CodePressCMS {
|
|||||||
$relativePath = $pagePath ? $pagePath . '/' . $item : $item;
|
$relativePath = $pagePath ? $pagePath . '/' . $item : $item;
|
||||||
|
|
||||||
if (is_dir($itemPath)) {
|
if (is_dir($itemPath)) {
|
||||||
$subdirs[] = [
|
$allItems[] = [
|
||||||
'name' => ucfirst($item),
|
'name' => ucfirst($item),
|
||||||
'path' => $relativePath,
|
'path' => $relativePath,
|
||||||
'url' => '?page=' . $relativePath
|
'url' => '?page=' . $relativePath,
|
||||||
|
'icon' => 'bi-folder',
|
||||||
|
'type' => 'directory'
|
||||||
];
|
];
|
||||||
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
|
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
|
||||||
$extractedTitle = $this->extractPageTitle($itemPath);
|
$extractedTitle = $this->extractPageTitle($itemPath);
|
||||||
$fileTitle = $extractedTitle ?: ucfirst(pathinfo($item, PATHINFO_FILENAME));
|
$fileTitle = $extractedTitle ?: ucfirst(pathinfo($item, PATHINFO_FILENAME));
|
||||||
$pathWithoutExt = preg_replace('/\.[^.]+$/', '', $relativePath);
|
$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,
|
'name' => $fileTitle,
|
||||||
'path' => $pathWithoutExt,
|
'path' => $pathWithoutExt,
|
||||||
'url' => '?page=' . $pathWithoutExt,
|
'url' => '?page=' . $pathWithoutExt,
|
||||||
'type' => pathinfo($item, PATHINFO_EXTENSION)
|
'icon' => $icon,
|
||||||
|
'type' => 'file'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display subdirectories
|
// Display all items in a single column
|
||||||
if (!empty($subdirs)) {
|
if (!empty($allItems)) {
|
||||||
$content .= '<div class="col-md-6">';
|
|
||||||
$content .= '<h3>📁 Mappen</h3>';
|
|
||||||
$content .= '<div class="list-group">';
|
$content .= '<div class="list-group">';
|
||||||
foreach ($subdirs as $subdir) {
|
foreach ($allItems as $item) {
|
||||||
$content .= '<a href="' . htmlspecialchars($subdir['url']) . '" class="list-group-item list-group-item-action">';
|
$content .= '<a href="' . htmlspecialchars($item['url']) . '" class="list-group-item list-group-item-action d-flex align-items-center">';
|
||||||
$content .= '<i class="bi bi-folder"></i> ' . htmlspecialchars($subdir['name']);
|
$content .= '<i class="bi ' . $item['icon'] . ' me-3"></i>';
|
||||||
|
$content .= '<span>' . htmlspecialchars($item['name']) . '</span>';
|
||||||
$content .= '</a>';
|
$content .= '</a>';
|
||||||
}
|
}
|
||||||
$content .= '</div></div>';
|
$content .= '</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>';
|
|
||||||
$hasContent = true;
|
$hasContent = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$content .= '</div>';
|
$content .= '</div>';
|
||||||
|
|
||||||
if (!$hasContent) {
|
if (!$hasContent) {
|
||||||
$content .= '<p>Deze map is leeg.</p>';
|
$content .= '<p>' . $this->t('directory_empty') . '.</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -726,8 +756,8 @@ class CodePressCMS {
|
|||||||
*/
|
*/
|
||||||
private function getError404() {
|
private function getError404() {
|
||||||
return [
|
return [
|
||||||
'title' => 'Page Not Found',
|
'title' => $this->t('page_not_found'),
|
||||||
'content' => '<h1>404 - Page Not Found</h1><p>The page you are looking for does not exist.</p>'
|
'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() {
|
public function render() {
|
||||||
$page = $this->getPage();
|
$page = $this->getPage();
|
||||||
$menu = $this->getMenu();
|
$menu = $this->getMenu();
|
||||||
$breadcrumb = $this->getBreadcrumb();
|
$breadcrumb = $this->generateBreadcrumb();
|
||||||
|
|
||||||
// Get homepage title
|
// Get homepage title
|
||||||
$homepageTitle = $this->getHomepageTitle();
|
$homepageTitle = $this->getHomepageTitle();
|
||||||
@ -764,30 +794,57 @@ class CodePressCMS {
|
|||||||
'default_page' => $this->config['default_page'],
|
'default_page' => $this->config['default_page'],
|
||||||
'homepage' => $this->config['default_page'],
|
'homepage' => $this->config['default_page'],
|
||||||
'homepage_title' => $homepageTitle,
|
'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_name' => $this->config['author']['name'] ?? 'CodePress Developer',
|
||||||
'author_website' => $this->config['author']['website'] ?? '#',
|
'author_website' => $this->config['author']['website'] ?? '#',
|
||||||
'author_git' => $this->config['author']['git'] ?? '#',
|
'author_git' => $this->config['author']['git'] ?? '#',
|
||||||
'seo_description' => $this->config['seo']['description'] ?? 'CodePress CMS - Lightweight file-based content management system',
|
'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
|
// File info for footer
|
||||||
if (isset($page['file_info'])) {
|
if (isset($page['file_info'])) {
|
||||||
$templateData['file_info'] = 'Created: ' . htmlspecialchars($page['file_info']['created']) .
|
$templateData['file_info'] = $this->t('created') . ': ' . htmlspecialchars($page['file_info']['created']) .
|
||||||
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
|
' | ' . $this->t('modified') . ': ' . htmlspecialchars($page['file_info']['modified']);
|
||||||
$templateData['file_info_block'] = '<span class="file-details"> | ' . $templateData['file_info'] . '</span>';
|
$templateData['file_info_block'] = '<span class="file-details"> | ' . $templateData['file_info'] . '</span>';
|
||||||
} else {
|
} else {
|
||||||
$templateData['file_info'] = '';
|
$templateData['file_info'] = '';
|
||||||
$templateData['file_info_block'] = '';
|
$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
|
// Check if content exists for guide link
|
||||||
$hasContent = !$this->isContentDirEmpty();
|
$hasContent = !$this->isContentDirEmpty();
|
||||||
@ -796,60 +853,35 @@ class CodePressCMS {
|
|||||||
// Don't show site title link on guide page
|
// Don't show site title link on guide page
|
||||||
$templateData['show_site_link'] = !$this->isContentDirEmpty() && !isset($_GET['guide']);
|
$templateData['show_site_link'] = !$this->isContentDirEmpty() && !isset($_GET['guide']);
|
||||||
|
|
||||||
// Load partials manually
|
// Load and render all templates with data
|
||||||
$hasContent = !$this->isContentDirEmpty() && !isset($_GET['guide']);
|
$layoutTemplate = file_get_contents($this->config['templates_dir'] . '/layout.mustache');
|
||||||
|
$headerTemplate = file_get_contents($this->config['templates_dir'] . '/assets/header.mustache');
|
||||||
$headerContent = file_get_contents($this->config['templates_dir'] . '/assets/header.mustache');
|
$navigationTemplate = file_get_contents($this->config['templates_dir'] . '/assets/navigation.mustache');
|
||||||
if (!$hasContent) {
|
$footerTemplate = file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache');
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine content type and load appropriate template
|
// Determine content type and load appropriate template
|
||||||
$contentType = $this->getContentType($page);
|
$contentType = $this->getContentType($page);
|
||||||
$contentTemplateFile = $this->config['templates_dir'] . '/' . $contentType . '_content.mustache';
|
$contentTemplateFile = $this->config['templates_dir'] . '/' . $contentType . '_content.mustache';
|
||||||
$contentTemplate = file_exists($contentTemplateFile) ? file_get_contents($contentTemplateFile) : '<div class="content">{{{content}}}</div>';
|
$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';
|
|
||||||
}
|
|
||||||
|
|
||||||
$contentTemplateFile = $this->config['templates_dir'] . '/' . $contentType . '_content.mustache';
|
// Render all templates with data
|
||||||
$contentTemplate = file_exists($contentTemplateFile) ? file_get_contents($contentTemplateFile) : '<div class="content">{{{content}}}</div>';
|
$renderedHeader = SimpleTemplate::render($headerTemplate, $templateData);
|
||||||
|
$renderedNavigation = SimpleTemplate::render($navigationTemplate, $templateData);
|
||||||
|
$renderedFooter = SimpleTemplate::render($footerTemplate, $templateData);
|
||||||
|
$renderedContent = SimpleTemplate::render($contentTemplate, $templateData);
|
||||||
|
|
||||||
$partials = [
|
// Replace partials in layout
|
||||||
'header' => file_get_contents($this->config['templates_dir'] . '/assets/header.mustache'),
|
$finalTemplate = str_replace('{{>header}}', $renderedHeader, $layoutTemplate);
|
||||||
'navigation' => file_get_contents($this->config['templates_dir'] . '/assets/navigation.mustache'),
|
$finalTemplate = str_replace('{{>navigation}}', $renderedNavigation, $finalTemplate);
|
||||||
'footer' => file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache'),
|
$finalTemplate = str_replace('{{>footer}}', $renderedFooter, $finalTemplate);
|
||||||
'content_template' => $contentTemplate
|
$finalTemplate = str_replace('{{>content_template}}', $renderedContent, $finalTemplate);
|
||||||
];
|
|
||||||
|
|
||||||
// Replace partials in template
|
// Render the final layout with all template data
|
||||||
$template = file_get_contents($this->config['templates_dir'] . '/layout.mustache');
|
$renderedLayout = SimpleTemplate::render($finalTemplate, $templateData);
|
||||||
$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
|
echo $renderedLayout;
|
||||||
$renderedTemplate = SimpleTemplate::render($template, $templateData);
|
|
||||||
echo $renderedTemplate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -857,30 +889,37 @@ class CodePressCMS {
|
|||||||
*
|
*
|
||||||
* @return string Breadcrumb HTML
|
* @return string Breadcrumb HTML
|
||||||
*/
|
*/
|
||||||
private function getBreadcrumb() {
|
private function generateBreadcrumb() {
|
||||||
if (isset($_GET['search'])) {
|
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 = $_GET['page'] ?? $this->config['default_page'];
|
||||||
$page = preg_replace('/\.[^.]+$/', '', $page);
|
$page = preg_replace('/\.[^.]+$/', '', $page);
|
||||||
|
|
||||||
if ($page === $this->config['default_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">';
|
||||||
$breadcrumb = '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">Home</a></li>';
|
|
||||||
|
// 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) {
|
foreach ($parts as $i => $part) {
|
||||||
$path .= ($path ? '/' : '') . $part;
|
$currentPath .= ($currentPath ? '/' : '') . $part;
|
||||||
$title = ucfirst($part);
|
$title = ucfirst($part);
|
||||||
|
|
||||||
if ($i === count($parts) - 1) {
|
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 {
|
} 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
|
// Root level folders
|
||||||
$html .= '<li class="nav-item dropdown">';
|
$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 .= '<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 .= '</a>';
|
||||||
$html .= '<ul class="dropdown-menu" aria-labelledby="' . $folderId . '">';
|
$html .= '<ul class="dropdown-menu" aria-labelledby="' . $folderId . '">';
|
||||||
$html .= $this->renderMenu($item['children'], $level + 1);
|
$html .= $this->renderMenu($item['children'], $level + 1);
|
||||||
@ -919,7 +958,7 @@ class CodePressCMS {
|
|||||||
// Nested folders in dropdown
|
// Nested folders in dropdown
|
||||||
$html .= '<li class="dropdown-submenu">';
|
$html .= '<li class="dropdown-submenu">';
|
||||||
$html .= '<a class="dropdown-item dropdown-toggle" href="#" id="' . $folderId . '">';
|
$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 .= '</a>';
|
||||||
$html .= '<ul class="dropdown-menu" aria-labelledby="' . $folderId . '">';
|
$html .= '<ul class="dropdown-menu" aria-labelledby="' . $folderId . '">';
|
||||||
$html .= $this->renderMenu($item['children'], $level + 1);
|
$html .= $this->renderMenu($item['children'], $level + 1);
|
||||||
|
|||||||
26
engine/lang/en.php
Normal file
26
engine/lang/en.php
Normal 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
26
engine/lang/nl.php
Normal 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'
|
||||||
|
];
|
||||||
@ -10,11 +10,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="site-info">
|
<div class="site-info">
|
||||||
<small class="text-muted">
|
<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>
|
<i class="bi bi-book"></i>
|
||||||
</a>
|
</a>
|
||||||
<span class="ms-2">|</span>
|
<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>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="?page={{default_page}}">
|
<a class="navbar-brand" href="?page={{default_page}}">
|
||||||
<img src="/assets/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
|
<img src="/assets/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
|
||||||
{{site_title}}
|
{{site_title}}
|
||||||
</a>
|
</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">
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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>
|
<i class="bi bi-search"></i>
|
||||||
</button>
|
</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>
|
||||||
|
|
||||||
<div class="collapse navbar-collapse" id="searchBar">
|
<!-- Mobile search bar -->
|
||||||
<form class="d-flex ms-auto" method="GET" action="">
|
<div class="collapse navbar-collapse d-lg-none" id="mobileSearch">
|
||||||
<input class="form-control me-2" type="search" name="search" placeholder="Search..." value="{{search_query}}">
|
<div class="container-fluid px-0">
|
||||||
<button class="btn btn-outline-light" type="submit">Search</button>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
<nav class="navigation-section border-bottom navigation-50-opacity">
|
<nav class="navigation-section">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row align-items-center">
|
<div class="row align-items-center">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<ul class="nav nav-tabs flex-wrap">
|
<ul class="nav nav-tabs flex-wrap">
|
||||||
<li class="nav-item">
|
<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}}
|
<i class="bi bi-house"></i> {{homepage_title}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@ -25,6 +25,133 @@
|
|||||||
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
|
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="/assets/css/bootstrap-icons.css" rel="stylesheet">
|
<link href="/assets/css/bootstrap-icons.css" rel="stylesheet">
|
||||||
<link href="/assets/css/style.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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{{>header}}
|
{{>header}}
|
||||||
|
|||||||
54
public/assets/css/mobile.css
Normal file
54
public/assets/css/mobile.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user