Plugins jQuery vs Web Components

Presente y futuro de dos formas de extender la web

Durante más de una década, los plugins de jQuery (los actuales de PHPRunner) fueron la forma dominante de extender la web con componentes reutilizables. Desde sliders hasta calendarios, pasando por validadores de formularios, jQuery ofrecía una API sencilla y un ecosistema inmenso.

Pero la web ha evolucionado. Hoy, los Web Components representan un estándar nativo, moderno y sostenible para crear componentes reutilizables sin depender de frameworks.

Este artículo compara ambas tecnologías desde una perspectiva técnica y práctica, y analiza cuál es su papel en el presente y hacia dónde se dirige el futuro.

1. Qué son los plugins de jQuery

Los plugins de jQuery surgieron como una forma rápida y sencilla de encapsular funcionalidad en la época en la que:

  • el DOM era inconsistente entre navegadores
  • no existían módulos ES
  • no existían clases nativas
  • no existía Shadow DOM
  • no existían Custom Elements

Un plugin típico:

(function($){
    $.fn.miPlugin = function(options){
        return this.each(function(){
            // lógica del plugin
        });
    };
})(jQuery);
Ventajas históricas
  • API extremadamente simple
  • Ecosistema enorme
  • Resolvían problemas reales de compatibilidad
  • Fácil de integrar en cualquier web
Limitaciones actuales
  • Dependen de jQuery (carga extra)
  • No aíslan estilos ni HTML
  • No son reutilizables fuera del DOM global
  • No funcionan bien en arquitecturas modernas (SPA, SSR, microfrontends)
  • No aprovechan capacidades modernas del navegador

2. Qué son los Web Components

Los Web Components son un estándar nativo del navegador, compuesto por:

  • Custom Elements
  • Shadow DOM
  • HTML Templates
  • ES Modules

Permiten crear componentes reutilizables sin frameworks:

class MiComponente extends HTMLElement {
    connectedCallback() {
        this.innerHTML = `<button>Hola</button>`;
    }
}
customElements.define('mi-componente', MiComponente);

Ventajas modernas
  • No requieren librerías externas
  • Encapsulan HTML, CSS y JS
  • Funcionan en cualquier framework (React, Vue, Angular, Svelte, Java, ASP,  PHP, WordPress…)
  • Son estándar W3C
  • Son ideales para microfrontends y aplicaciones híbridas
  • Funcionan igual en desarrollo y producción
Limitaciones
  • API más compleja que jQuery
  • No todos los desarrolladores están familiarizados
  • El Shadow DOM puede complicar estilos globales
  • Requieren build tools si se usan con frameworks modernos (Svelte, React, etc.)

3. Comparación directa

Criterio Plugins jQuery Web Components
Dependencias jQuery obligatorio Nativo, sin dependencias
Encapsulación Ninguna Shadow DOM
Reutilización Solo en DOM global En cualquier framework o web
Rendimiento Medio Alto
Compatibilidad futura En declive Estándar W3C
Mantenimiento Ecosistema abandonado En crecimiento
Integración Fácil en webs antiguas Fácil en webs modernas
Soporte a largo plazo No garantizado Garantizado por navegadores

4. Presente: ¿qué se usa hoy?

En webs antiguas o legacy

Los plugins jQuery siguen existiendo porque:

  • ya están integrados
  • funcionan
  • migrar cuesta tiempo

Pero no se crean plugins nuevos.

En webs modernas

Los Web Components están creciendo rápidamente, especialmente en:

  • aplicaciones híbridas (PHP + JS moderno)
  • microfrontends
  • sistemas de diseño (Design Systems)
  • componentes compartidos entre equipos
  • integraciones con CMS (WordPress, Drupal, Prestashop)
  • frameworks modernos (Svelte, React, Vue)

5. Futuro: ¿qué dominará?

La tendencia es clara:

jQuery → mantenimiento mínimo
Web Components → estándar dominante

Los navegadores ya soportan Web Components de forma nativa, y los frameworks modernos (Svelte, React, Vue, Angular) pueden generar Web Components como output final.

Esto significa:

  • un componente creado hoy en Svelte 5 puede usarse en cualquier web dentro de 10 años
  • un plugin jQuery creado hoy probablemente no sobrevivirá

6. Conclusión

Los plugins de jQuery fueron una solución brillante para su época, pero hoy pertenecen al pasado. Los Web Components representan el presente y el futuro de los componentes reutilizables en la web.

La transición no es solo tecnológica: es un cambio de mentalidad hacia componentes nativos, encapsulados, interoperables y duraderos.

Para proyectos nuevos, la recomendación es clara:

Construir componentes como Web Components, idealmente usando un framework moderno como Svelte 5 para simplificar el desarrollo.

Estar atentos a las novedades sobre este tema
En los siguientes artículos os mostraré cómo estos nuevos componentes se pueden crear con Svelte 5 y cómo los podemos utilizar en PHPRunner y por extensión, en cualquier otro sistema que genere páginas HTML, JavaScript y CSS.

Guía para construir Plugins de PHPRunner
He creado una guía para construir los plugins de PHPRunner. Está ubicada en GITHUB, donde están todos los plugins que yo os facilito de forma gratuita.

S-022 – Ordenación de Jerarquías en árbol

Otra solución que tenía en el ámbito del lenguaje de PHP era la ordenación de jerarquías tipo árbol.  Es muy habitual contar con esta organización en tareas de un Gantt, estructura de personal en una compañía, etc., por lo que requerimos de un algoritmo para obtener esa jerarquía a partir de datos planos donde sólo se dispone del registro «padre» para montar esa estructura de árbol.

Objetivo

Disponer de un algoritmo en JavaScript que facilite un estructura jerárquica partiendo de registros que identifican cuál es el registro «padre».

DEMO: https://fhumanes.com/my-jerarquia/

Los campos con fondo amarillo son calculados.

(1) .- Es el orden de los registros según su estructura y su fecha de inicio.
(2).- Identificador de orden. Es el orden, teniendo en cuenta el nivel de dependencia.
(3).- Es el nivel de profundidad de la estructura para ese registro.
(4).- Indica si es un nodo maestro, que tiene registros dependiente de él.

Si te interesa esta información sigue leyendo el artículo en este enlace.

S021 – Drag and Drop (Arrastrar y Soltar) en Svelte 5

Esta funcionalidad de «arrastrar y soltar» no es de las primeras cosas que pruebas en un nuevo entorno de programación, pero es importante para determinadas aplicaciones, si deseas que el interfaz de la aplicación aparezca como algo moderno.

Repasando las aplicaciones realizadas en Web con PHPRunner, vi que tenía una, de la que me sentía muy contento y que me faltaba esta pieza/solución. La aplicación es KANBAN.

La parte del interfaz que me faltaba era la de «Drag and Drop» o «Arrastrar y Soltar», para cambiar de estado a los incidentes o tareas.

Objetivo

Seleccionar una librería de DnD (Drag and Drop) para añadir esta funcionalidad a los desarrollos de Svelte 5.

DEMO:  https://fhumanes.com/my-fluid-dnd/

Solución Técnica

El producto que creí que tenía todas las características que necesitaba es Fluid DnD y el ejercicio que he hecho son prácticamente todos los ejemplos que tiene en el blog de documentación.

Si estás interesado en este tema, sigue leyendo el artículo en este enlace.

S-020 – Subida de múltiples ficheros

Para mí, el hecho de tener un buen control sobre el componente que nos permite hacer Upload de los ficheros es muy importante, más, cuando quieres que sean un ficheros con extensión determinada y que si son imágenes las puedas redimensionar y ajustar a las características que precises.

Este ejercicio va de eso, subir ficheros de forma elegante y determinando en número de ellos que se pueden subir en la misma transacción.

Objetivo

Disponer de un componente que permita subir ficheros de forma eficiente (asíncrona)  y fácil (arrastrar), controlando características de los ficheros y redimensionando o convirtiendo características de los ficheros.

DEMO:  https://fhumanes.com/multi-files/

Si estás interesado en este artículo sigue leyéndolo en este enlace.

Gestor de Proyectos (Actualizado 21/04/2026)

Creo que este es el producto que más éxito ha tenido de mi blog.

Como estaba un poco antiguo y el Grantt era de una solución que no ha sido actualizada en los últimos años, consideré que debería actualizarlo y para ello he seleccionado la solución de Gantt de AnyChart. Como he indicado en otros artículos, la solución de AnyChart es de una altísima calidad y con la licencia del producto que incluye PHPRunner, te facilita el uso de todos sus productos.

Además del cambio de la solución de Gantt, he incluido estas otras funcionalidades.

  • Poder visualizar y editar todas las características de las tareas desde el mismo Gantt.
  • He corregido un error en el calculo de la fecha final partiendo de los días de la tarea. El problema era la edición del formato de fecha para español e inglés.
  • He incluido la funcionalidad de Backup y Restore de la base de datos, desde la misma aplicación. Esto me facilitará la restauración de los datos iniciales, ya que al final los usuarios «destruyen» el juego de pruebas con que lo instalo.
  • He pasado y dejado la versión en la versión 10.91 de PHPRunner. Desde ella, podría ser migrada a versiones 11 del producto.
  • También, voy a dejar una versión generada, para que quién no tenga PHPRunner pueda instalarlo y configurarlo, apoyándose en el fichero «config.php» que tiene los datos de conexión a la Base de Datos. He tenido varios usuarios que estaban interesados en la solución, pero no contaban con PHPRunner.

El código que genera el Gantt y que define los eventos del mismo es:

gantt_project_anychart.php
<?php
$keyMaster= $_SESSION['project_id'];
$html = <<<EOT
<script src="anychart_8.13.1/js/anychart-base.min.js"></script>
<script src="anychart_8.13.1/js/anychart-ui.min.js"></script>
<script src="anychart_8.13.1/js/anychart-exports.min.js"></script>
<script src="anychart_8.13.1/js/anychart-gantt.min.js"></script>
<script src="anychart_8.13.1/js/anychart-data-adapter.min.js"></script>
<link href="anychart_8.13.1/css/anychart-ui.min.css" type="text/css" rel="stylesheet">
<link href="https://cdn.anychart.com/releases/8.13.1/fonts/css/anychart-font.min.css" type="text/css" rel="stylesheet">        
EOT;
if ($_SESSION['language'] == "Spanish") {
$language = 'es-es';
$dateFormat='dd/MM/yyy';
$textName = 'Nombre';
$textProgress = 'Progreso';
$html .= <<<EOT
<script src="anychart_8.13.1/locales/es-es.js"></script>
EOT;
} else {
$language='en-us';
$dateFormat='MM/dd/yyy';
$textName = 'Name';
$textProgress = 'Progress';
$html .= <<<EOT
<script src="anychart_8.13.1/locales/en-us.js"></script>
EOT;
}

$records =[];

// Read Project the Project
$sql="SELECT (999000000+p.project_id) task_id,
    concat(p.short_name,' - ',p.name) name,
    DATE_FORMAT(p.`start_date`,'%Y-%m-%d') `start_date`,
    DATE_FORMAT(ifnull(p.`actual_end_date`,p.`end_date`),'%Y-%m-%d') `end_date`,
    0  `dynamic`,
    '' `related_url`,
    0  `milestone`,
    p.`percent_complete`,
    0   parent,
    0   dependent,
    p .`description`,
    ifnull(u.login,'') creator
FROM projects p
  inner join users u on (p.owner=u.user_id)
WHERE p.project_parent = $keyMaster";
$resql = DB::Query($sql);

 while ($row = $resql->fetchAssoc()) {
            $item = [
                "id" =>  $row['task_id'],
                "name" => addslashes($row['name']),
                "actualStart" => strtotime($row['start_date']) * 1000,
                "actualEnd" => strtotime($row['end_date']) * 1000,
                "progressValue"=> $row['percent_complete'] / 100,
                "collapsed" => true,
                "task_link" => $row['related_url'],
                "task_owner" => $row['creator'],
                "task_description" => $row['description'],
                "task_note" => addslashes(str_replace(hex2bin('0a'),'',str_replace(hex2bin('0d'),'',str_replace(PHP_EOL,'<br />', $task_description)))),
                "actual"=> [
                    "fill"=> "#4CAF50",     // color de la barra de fondo
                    "stroke"=> "#2E7D32"    // borde opcional
                    ],
                "progress"=> [
                    "fill"=> "#319435"      // color del % ejecutado
                ],
            ];
            // Añadir al array principal
            $records[] = $item;       
    }
    
// Read Task the Project
$sql="SELECT
        t.`task_id`,
        t.`name`,
        DATE_FORMAT(t.`start_date`,'%Y-%m-%d') `start_date`,
        DATE_FORMAT(t.`end_date`,'%Y-%m-%d') `end_date`,
        t.`dynamic`,
        t.`related_url`,
        t.`milestone`,
        t.`percent_complete`,
        ifnull(t.`parent`,0) parent,
        ifnull(t.`dependent`,0) dependent,
        t.`description`,
        ifnull(u.login,'') creator
FROM tasks t
        inner join users u on (t.creator=u.user_id)
WHERE t.`projects_project_id` = $keyMaster
ORDER BY t.`order`";
$resql = DB::Query($sql);

 while ($row = $resql->fetchAssoc()) {
            $item = [
                "id" =>  $row['task_id'],
                "name" => addslashes($row['name']),
                "actualStart" => strtotime($row['start_date']) * 1000,
                "actualEnd" => strtotime($row['end_date']) * 1000,
                "progressValue"=> $row['percent_complete'] / 100,
                "collapsed" => true,
                "task_link" => $row['related_url'],
                "task_owner" => $row['creator'],
                "task_description" => $row['description'],
                "task_note" => addslashes(str_replace(hex2bin('0a'),'',str_replace(hex2bin('0d'),'',str_replace(PHP_EOL,'<br />', $task_description)))),              
            ];
            // Si tiene parent
            if (!empty($row['parent'])) {
                $item["parent"] = $row['parent'];
            }
            // Si tiene conexión
            if (!empty($row['dependent'])) {
                $item["connectTo"] = $row['dependent'];
            }
            // Si es Milestone
            if (!empty($row['milestone'])) {
                $item["actualEnd"] = strtotime($row['start_date']) * 1000;
            }
            
            // Añadir al array principal
            $records[] = $item;           
    }

 $data = json_encode($records, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK);

$html .= <<<EOT
<style>
    #container1 {
        margin: 0;
        padding: 0;
        width: 100%;
        height: 45vh;
        display: flex;
        flex-direction: column;
    }

</style>

<div id="container1"></div>

<script>
    let chart;

    const data = $data; 

    anychart.onDocumentReady(function () {
        
        // Activar locale español (funciona con CDN)
        anychart.format.inputLocale('$language');
        anychart.format.inputDateTimeFormat('$dateFormat'); //Like '12/03/2012'
        anychart.format.outputLocale('$language');

        var treeData = anychart.data.tree(data, "as-table");
        // console.log("treeData: ", treeData);

        chart = anychart.ganttProject();

        // Licencia (si la tienes)
        window.anychart.licenseKey('XXXXXXXXX-XXXXXXXXX-XXXXXXXXX');
        var credits = chart.credits();
        credits.enabled(false);
   
        // Activa el panel de controles (export, print, fullscreen)
        var exports = chart.exports();
        // console.log("exports: ", exports);
        
        // Set client side export settings.
        exports.clientside({
        'path': 'https://cdn.anychart.com/3rd/', // path to dependencies
        'enabled': true, // clientside export enabled
        'fallback': false // fallback to server side exporting
        });

        chart.data(treeData);

        // set chart row's height
        chart.defaultRowHeight(30);

        // set start splitter position settings
        chart.splitterPosition(324);

        // get chart data grid
        var dataGrid = chart.dataGrid();

        // set data grid's first column settings
        dataGrid.column(0).width(30).labels({
        hAlign: 'center'
        });

        // set data grid's seconds column settings
        dataGrid
        .column(1)
        .collapseExpandButtons(false) // disable collapse/expand buttons
        .width(180)
        .title('$textName');

        // create data grid's third column and set it's settings
        dataGrid.column(2).width(80).title('$textProgress');

        // create column with collapse/expand buttons
        dataGrid
        .column(3)
        .title(null)
        .width(30)
        .collapseExpandButtons(true);

        // set column labels overrider
        dataGrid.column(0).labelsOverrider(labelsTextSettings);
        dataGrid.column(1).labelsOverrider(labelsTextSettings);
        dataGrid.column(2).labelsOverrider(labelsTextSettings);

        // set labels formatter for the third column
        dataGrid
        .column(2)
        .labels()
        .format(function () {
            if (this.item.numChildren()) {
            // formatter for groupping tasks data grid labels
            if (this.progress !== 0) {
                return this.progress < 1 ? 'In progress' : 'Done';
            }
            }
            if (this.item.get('actualEnd')) {
            // format labels in group
            return Math.round(this.progress * 100) + '%';
            }
            if (this.item.getParent()) {
            // formatter for milestones
            return parseInt(
                this.item.getParent().get('progressValue')
            ) === 100
                ? 'Ok'
                : '';
            }
        });

        // get data grid buttons
        var button = dataGrid.buttons();

        // set buttons width and height
        button.width(20).height(15);

        // disable button's background for every state
        button.background(false);
        button.hovered().background(false);
        button.selected().background(false);

        // set data grid button's content using function
        var buttonContent = function (path) {
        // get center of button's path
        var centerX = this.width / 2;
        var centerY = this.height / 2;

        // draw arrow in path
        if (this.state === 'selected') {
            path
            .clear()
            .moveTo(3, 4)
            .lineTo(centerX, this.height - 2)
            .lineTo(this.width - 3, 4)
            .stroke('#00a8e0', 3, 'solid', 'miter', 'round');
        } else {
            path
            .clear()
            .moveTo(6, 2)
            .lineTo(this.width - 4, centerY + 1)
            .lineTo(6, this.height)
            .stroke('#748a8d', 3, 'solid', 'miter', 'round');
        }
        };

        // set function as content of button
        button.content(buttonContent);
        button.selected().content(buttonContent);

        // get timeline
        var timeline = chart.getTimeline();
        // get timeline's scale
        var scale = timeline.scale();
        
        // configure the scale
        // scale.zoomLevels([["week", "month", "quarter"]]);
        scale.zoomLevels([["week",  "quarter"]]);
            
        // Set scale maximum and minimum.
        scale.minimumGap(10,true);
        scale.maximumGap(15,true);

        // zoom chart all dates range
        // chart.fitAll();
        // zoom the timeline to the given units
        chart.zoomTo("week", 25, "first-date");  // Ponemos 25 semanas de visualización
  
        // Crear un marcador vertical
        const todayMarker = timeline.lineMarker();
        // Fecha fija: 2010-02-15
        const fechaFija = new Date().getTime();
        // const fechaFija = new Date("2010-05-02").getTime();

        // Configurar el marcador
        todayMarker
            .value(fechaFija)          // fecha fija en milisegundos
            .stroke('#ff0000', 2)      // color y grosor
            .scale(timeline.scale());  // importante: usar la escala del timeline

        // hide groupping tasks progress bars
        timeline.groupingTasks().progress().fill(null).stroke(null);

        timeline
        .groupingTasks()
        .progress()
        .selected()
        .fill(null)
        .stroke(null);

        // set shapes for groupping tasks rendering
        timeline
        .groupingTasks()
        .rendering()
        .shapes([
            {
            name: 'task',
            shapeType: 'path',
            disablePointerEvents: false
            }
        ]);

        // set custom drawer for groupping tasks
        timeline.groupingTasks().rendering().drawer(groupingTaskDrawer);

        // set groupping tasks bar's position
        timeline.groupingTasks().anchor('center').position('center');

        // set groupping tasks labels settings
        timeline
        .groupingTasks()
        .labels()
        .format(function () {
            return this.name;
        })
        .fontWeight('bold')
        .fontColor('#fff')
        .anchor('center')
        .position('center');

        // set timeline tasks fill and stroke settings
        timeline.tasks().selected().fill('#1876d1');
        timeline.tasks().selected().stroke('#1876d1');

        // set timeline tasks progress fill and stroke settings
        timeline.tasks().progress().selected().fill('#203951');
        timeline.tasks().progress().selected().stroke('#203951');

        chart.container("container1");
        chart.draw();

        // set "collapsed" data value after row collapse/expand
        chart.listen('rowCollapseExpand', function (e) {
            e.item.set('collapsed', e.collapsed);
        });  
        
        // configure tooltips of the timeline
        chart.getTimeline().tooltip().useHtml(true);    
        chart.dataGrid().tooltip().useHtml(true); 
        chart.getTimeline().tooltip().format(
          "<span style='font-weight:600;font-size:12pt'>" +
          "{%actualStart}{dateTimeFormat:dd MMM} - " +
          "{%actualEnd}{dateTimeFormat:dd MMM}</span><br><br>" +
          "Progress: {%progress}<br>" +
          "Manager: {%task_owner}"
        );
                
        // listen for task edit event
        chart.listen("rowDblClick", function (e) {
            const item = e.item; // tarea seleccionada
            const id = item.get("id");
            const name = item.get("name");
            const start = item.get("actualStart");
            const end = item.get("actualEnd");
            const progress = item.get("progressValue");

            console.log("Tarea seleccionada:", { id, name, start, end, progress });

            // Aquí llamas a tu modal o lógica de edición
            editarTarea({ id, name, start, end, progress });
        });

        // Captura de Tarea desde TimeLine
        /*
        chart.getTimeline().listen("pointClick", function (e) {
            const item = e.item;
            console.log("Click en barra:", item.get("id"));
            editarTarea({
                id: item.get("id"),
                name: item.get("name"),
                start: item.get("actualStart"),
                end: item.get("actualEnd"),
                progress: item.get("progressValue")
            });
        });
        */
        
        // set tasks labels settings
        timeline.tasks().labels().useHtml(true);
        timeline.tasks().labels()
            .format("- <span style='color:#ffffff'>{%progress}</span>")
            .anchor('center')
            .position('center');



    });

    // custom drawer function for the groupping tasks
    function groupingTaskDrawer() {
        var path;
        var itemBounds;

        // get path from shapes
        path = this.shapes.task;

        // get recommended bounds for drawing
        var bounds = this.predictedBounds;

        // get shift value
        var shift = halfShift(path.strokeThickness());

        // set bar's bounds
        itemBounds = anychart.math.rect(
            Math.round(bounds.left) - shift,
            Math.round(bounds.top) - 5 - shift,
            Math.round(bounds.width) - shift,
            21 - shift
        );

        // get progress value from data
        var progress = parseInt(this.item.get('progressValue')) / 100;

        // set path's fill and stroke settings
        path.fill(function () {
            if (progress !== 0) {
            return progress < 1 ? '#f4a700' : '#27a858';
            }
            return '#e51a23';
        });
        path.stroke(null);

        // draw rounded rectangle on the path
        anychart.graphics.vector.primitives.roundedRect(
            path,
            itemBounds,
            0
        );
    }

    function halfShift(strokeThickness) {
        return strokeThickness % 2 ? 0.5 : 0;
    }

    // labels overrider function
    function labelsTextSettings(label, item) {
        //  filter data items with "collapsed" = true
        var collapsedItems = chart.data().filter(function (item) {
            return item.get('collapsed');
        });
        if (item.numChildren()) {
            // override only groupping tasks labels
            // set label's weight
            label.fontWeight('bold');

            // set label's color
            label.fontColor(
            collapsedItems.indexOf(item) > -1 ? '#748a8d' : '#00a8e0'
            );
        }
    }

    // Actualizar tarea después de editarla
    function actualizarTarea(id, cambios) {
        const dataItem = chart.data().search("id", id);

        if (dataItem) {
            Object.entries(cambios).forEach(([key, value]) => {
                dataItem.set(key, value);
            });

            chart.draw(); // refresca el gráfico
        }
    }
    /* ejemplo de actualización de tarea:
    actualizarTarea("step1", {
        actualStart: new Date("2024-05-01").getTime(),
        actualEnd: new Date("2024-05-10").getTime(),
        progressValue: "80%"
    });
    */

    // Función para abrir modal de visualización y edición 
    function editarTarea(item ) {
        console.log("Editar tarea:", item);
        const isFullScreen = !!document.fullscreenElement;  // Controla si está en pantalla completa
        if (!isFullScreen && item.id < 10000000 ) {  //sólo cuando no está pantalla comleta y no en proyecto dependiente,  se "levanta" ventana popup
            var Ev_id = item.id;
            var title = item.name;
            var url = "tasks_view.php?page=view_gantt&editid1="+Ev_id;
            var header = '<h2 data-itemtype="view_header" data-itemid="view_header" data-pageid="10">'+'Tarea: '+ title+'</h2>' ;
            window.popup = Runner.displayPopup({
                url: url,
                width: 1000,
                height: 600,
                header: header
               });
        }
    };
  
</script>
EOT;
echo $html;
?>

Para los que no dispongan de PHPRunner, debéis tener en cuenta que el KIt que os dejo requiere:

  • Web Server Apache o similar, con PHP 8.2 o PHP 8.3
  • MySQL o Maria DB.

Las instrucciones de instalación son simples:

  • Descargar el KIT de instalación sin PHPRunner.
  • Desplegar el zip de la aplicación en el directorio del «document Root» de tu web Server, por ejemplo, directorio «project».
  • Crear una base de datos, por ejemplo «project» y crear todos los objetos con el Backup del modelo de datos.
  • Configurar el fichero «config.php», que si has instalado en el directorio «project» sería el path «project/config.php» y ahí escribir los datos de la conexión a la base de datos.
  • Con eso, ya tienes el producto instalado y el ususario por defecto es «admin»/»admin». Con este usuario, ya puedes cambiar el resto de configuración, así como los usuarios existentes o nuevos.

Como he tenido que recordar cómo estaba hecho el producto, en estos momentos puedo resolver cualquier error que detectéis o ampliar la solución a cualquier otro requerimiento que el producto no tenga.
Si necesitáis algo, escribirme al email [email protected]

Para acceder a toda la información del artículo, haz clic en este enlace.

Blog personal para facilitar soporte gratuito a usuarios de React y PHPRunner