Ticker

6/recent/ticker-posts

Bootstrap: Sidebars Collapsible

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
]);


Archivo view\admin\layout\sidebar.php
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, '&amp;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;')
                .replace(/"/g, '&quot;')
                .replace(/'/g, '&#039;');
        };

        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>


Publicar un comentario

0 Comentarios