En est tutorial elaboramos un componente Sidebars con la opción de Collapsible (collapse) por cada principal grupo o titulo del Sidebar (Grupo de General, Contenido, Comunicacion y Usuarios):
Archivo config\config.php
<?php
namespace Config;
class AppConfig {
// URL base del sitio
public static string $baseUrl = '/hadson_tech';
// URL del sitio (sin barra al final)
public static function getBaseUrl(): string {
return self::$baseUrl;
}
// URL completa para assets
public static function asset(string $path): string {
return self::$baseUrl . '/' . ltrim($path, '/');
}
// URL completa para rutas
public static function url(string $path = ''): string {
return self::$baseUrl . '/' . ltrim($path, '/');
}
}
Archivo api\sidebar.php
<?php
// api/sidebar.php - Estructura dinamica de navegacion para el panel admin
session_start();
require_once __DIR__ . '/../autoload.php';
require_once __DIR__ . '/../config/database.php';
use Config\AppConfig;
use Models\Contact;
header('Content-Type: application/json; charset=utf-8');
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode([
'success' => false,
'message' => 'No autorizado.'
]);
exit;
}
$baseUrl = rtrim(AppConfig::getBaseUrl(), '/');
$currentRoute = strtolower(trim((string)($_GET['route'] ?? 'dashboard')));
$unread = (new Contact())->count(['leido' => 0]);
$sections = [
[
'title' => 'General',
'items' => [
['route' => 'dashboard', 'label' => 'Dashboard', 'icon' => 'fa-solid fa-gauge-high'],
['route' => 'configuracion', 'label' => 'Configuracion', 'icon' => 'fa-solid fa-gear']
]
],
[
'title' => 'Contenido',
'items' => [
['route' => 'menu', 'label' => 'Menu', 'icon' => 'fa-solid fa-bars'],
['route' => 'hero', 'label' => 'Hero', 'icon' => 'fa-solid fa-house'],
['route' => 'services', 'label' => 'Servicios', 'icon' => 'fa-solid fa-briefcase'],
['route' => 'projects', 'label' => 'Proyectos', 'icon' => 'fa-solid fa-folder-open'],
['route' => 'integrations', 'label' => 'Integraciones', 'icon' => 'fa-solid fa-plug'],
['route' => 'stack', 'label' => 'Stack', 'icon' => 'fa-solid fa-code'],
['route' => 'pricing', 'label' => 'Precios', 'icon' => 'fa-solid fa-tag'],
['route' => 'testimonials', 'label' => 'Testimonios', 'icon' => 'fa-solid fa-quote-right'],
['route' => 'faq', 'label' => 'FAQ', 'icon' => 'fa-solid fa-circle-question'],
['route' => 'cta', 'label' => 'CTA', 'icon' => 'fa-solid fa-bullhorn'],
['route' => 'footer', 'label' => 'Footer', 'icon' => 'fa-solid fa-copyright']
]
],
[
'title' => 'Comunicacion',
'items' => [
['route' => 'contact', 'label' => 'Mensajes', 'icon' => 'fa-solid fa-envelope', 'badge' => $unread],
['route' => 'newsletter', 'label' => 'Suscriptores', 'icon' => 'fa-solid fa-newspaper']
]
],
[
'title' => 'Usuarios',
'items' => [
['route' => 'users', 'label' => 'Usuarios', 'icon' => 'fa-solid fa-users']
]
]
];
echo json_encode([
'success' => true,
'baseUrl' => $baseUrl,
'currentRoute' => $currentRoute,
'logoutUrl' => $baseUrl . '/admin/logout',
'sections' => $sections
]);
Código sin aplicar Collapsible
<!-- Sidebar -->
<div class="db-sidebar" id="dbSidebar" data-current-route="<?php echo htmlspecialchars($route ?? 'dashboard', ENT_QUOTES, 'UTF-8'); ?>">
<div class="db-logo">
<div class="logo-i"><i class="fa-brands fa-connectdevelop"></i></div>
<span>Hadson<span class="tech">.Tech</span></span>
</div>
<nav class="db-nav" id="adminSidebarNav">
<div class="db-nav-section">Cargando</div>
<a href="#" class="db-nl">
<i class="fa-solid fa-spinner fa-spin"></i> Cargando menú...
</a>
</nav>
<div class="db-bottom">
<a href="<?php echo $baseUrl; ?>/admin/logout" class="db-nl" style="color:#f87171">
<i class="fa-solid fa-right-from-bracket"></i> Cerrar sesión
</a>
</div>
</div>
<script>
(function() {
const body = document.body;
const sidebarNav = document.getElementById('adminSidebarNav');
const baseUrl = body?.dataset?.baseUrl || '';
const currentRoute = body?.dataset?.adminRoute || 'dashboard';
if (!sidebarNav || !baseUrl) {
return;
}
const safeText = function(text) {
return String(text ?? '')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
const isRouteActive = function(itemRoute, activeRoute) {
const item = String(itemRoute || '').toLowerCase();
const active = String(activeRoute || '').toLowerCase();
return active === item || active.indexOf(item + '/') === 0;
};
const renderSidebar = function(payload) {
const sections = Array.isArray(payload.sections) ? payload.sections : [];
const fragment = [];
sections.forEach(section => {
fragment.push('<div class="db-nav-section">' + safeText(section.title) + '</div>');
(section.items || []).forEach(item => {
const activeClass = isRouteActive(item.route, payload.currentRoute || currentRoute) ? ' active' : '';
const href = baseUrl + '/admin/' + String(item.route || '').replace(/^\/+/, '');
const badge = typeof item.badge !== 'undefined' && item.badge !== null
? '<span class="db-badge">' + safeText(item.badge) + '</span>'
: '';
fragment.push(
'<a href="' + safeText(href) + '" class="db-nl' + activeClass + '">' +
'<i class="' + safeText(item.icon || 'fa-solid fa-circle') + '"></i> ' +
safeText(item.label) +
badge +
'</a>'
);
});
});
sidebarNav.innerHTML = fragment.join('');
};
fetch(baseUrl + '/api/sidebar?route=' + encodeURIComponent(currentRoute), {
headers: {
'Accept': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error('No fue posible cargar el menú.');
}
return response.json();
})
.then(data => {
if (!data.success) {
throw new Error(data.message || 'No fue posible cargar el menú.');
}
renderSidebar(data);
})
.catch(() => {
sidebarNav.innerHTML = '<div class="db-nav-section">General</div>' +
'<a href="' + safeText(baseUrl + '/admin/dashboard') + '" class="db-nl active">' +
'<i class="fa-solid fa-gauge-high"></i> Dashboard</a>';
});
})();
</script>
0 Comentarios