DÍA 14 / 2018

Cómo crear diferentes Sistemas de Votación para tu Web

Un pequeño tutorial de cómo crear 4 diferentes sistemas de votación para artículos o productos en nuestra web con un poco de HTML, Javascript y mucho CSS nos darán la posibilidad de agregar estos elementos a nuestra página… porque no todo va a ser WordPress y sus plugins.


Hola amig@s, este es mi 5º año participando en Octuweb y, para mí, siempre es un orgullo hacer un hueco -a veces es muy difícil encontrarlo- para dejar mi granito de arena y aportar algún recurso desde mi humilde experiencia. Agradezco a Félix el tenerme siempre en cuenta a la hora de hacer la convocatoria.

Nunca he pretendido ser ídolo de multitudes pero si, con un simple artículo, puedo ayudar a alguien a simplificar su tarea, he logrado mi objetivo. Este blog nos brinda la oportunidad de aportar algo de valor a algunos elegidos y ser uno de ellos me llena de satisfacción.

Rollo dicho, nunca me gustó escribir artículos dando consejos, no creo estar en condiciones de darle un consejo a nadie y no me gustaría cargar con la culpa interior de aconsejar mal a alguien, por eso, mis artículos siempre son sobre pequeños tutoriales de cómo crear cosas.

Y en este caso, este tutorial parecerá un poco simple pero, curiosamente, es uno de los temas por los que más consultas recibo en mi blog (o recibía hasta que el RGPD y Disqus tardaron en ponerse de acuerdo) y por eso quiero compartir 4 mini tutoriales de cómo votar contenidos en nuestra web.

Vivamos en Democracia

Vamos a dejarlo claro, no soy muy fan de creer que estos sistemas aporten algo… dudo mucho que una persona decida una compra de un producto en una tienda online por la cantidad de estrellas que tiene… todos sabemos que estos valores son manipulables. Tampoco me va a gustar más o menos un contenido por la cantidad de pulgares arriba, caras felices, pulgares abajo o caras tristes que tenga lo que estoy viendo. Seré yo quién decida la puntuación que le pongo a lo que leo.

Pero, viendo el lado positivo, estos sistemas nos pueden aportar una oportunidad de conocer cómo reacciona la gente a lo que le proponemos cuando visita nuestra web, lee nuestro artículo o busca un producto y este es el lado con el que nos quedaremos, con que estos sistemas son útiles y nos aportan valor.

Antes de nada

Para este tutorial he usado una base de datos MySQL para almacenar los votos recibidos y una imagen a modo de sprite con los iconos utilizados. La estructura de la tabla aparece a continuación y la imagen la puedes descargar desde este enlace.

CREATE TABLE `votos` (
  `id` INT(11) NOT NULL,
  `idarticulo` INT(11) NOT NULL,
  `tipo` VARCHAR(20) NOT NULL,
  `puntaje` INT(11) NOT NULL,
  `votos` INT(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

ALTER TABLE `votos`
  ADD PRIMARY KEY (`id`),
  ADD KEY `idarticulo` (`idarticulo`),
  ADD KEY `tipo` (`tipo`);

Vamos al grano

Cerrando toda la introducción que hice -debo confesarlo, soy argentino de nacimiento, hablo hasta por los codos-, lo que vamos a ver son 4 sencillas pero diferentes formas de ofrecerle al usuario puntuar nuestro contenido.

Nota importante: durante los códigos que verás más abajo, se hacen llamadas a funciones genéricas que son “votar” y “alerta” en el código javascript. Estas funciones estarán pegadas al final de todo. De todas maneras, te puedes bajar el tutorial completo desde este enlace.

Votación por Pulgar Arriba y Pulgar Abajo

Desde la época de los romanos el pulgar arriba o abajo marcó el destino de las personas pero en este caso, lo usaremos para indicar cuántos votos positivos y negativos recibe nuestro contenido.

Para ello, además de crear una base de datos (porque vamos a almacenar la cuenta), crearemos todas las funciones necesarias para mostrar el recuento actual, daremos opción a votar (no hemos puesto ninguna forma de bloqueo al voto repetitivo aunque sería muy fácil añadirla) y ejecutaremos mediante una llamada en Ajax nuestro voto.

Partiendo del primer paso en el que mostramos los elementos nos encontramos con:

<ul class="lkdlk">
    <li class="like"><a href="#" data-voto="like" title="&iexcl;ME GUSTA!"><span class="icon"></span><span class="count"><?php echo ($votos["like"]["puntaje"]?$votos["like"]["puntaje"]:0); ?></span></a></li>
    <li class="dislike"><a href="#" data-voto="dislike" title="NO ME GUSTA"><span class="icon"></span><span class="count"><?php echo ($votos["dislike"]["puntaje"]?$votos["dislike"]["puntaje"]:0); ?></span></a></li>
</ul>
Importante en este código: el atributo data-voto que lleva cada uno de los enlaces ya que serán los encargados de informarle al javascript qué hemos hecho entre “me gusta” o “no me gusta”.
$("ul.lkdlk li a").click(function(e)
{
    e.preventDefault(); /* Evitamos el # en la barra de direcciones */
    var voto=$(this).data("voto"); /* Obtenemos el resultado del voto: like o dislike */
    var objeto=$(this).closest("li").find(".count"); /* Obtenemos el elemento para cambiar el valor una vez realizado el voto */
    if (voto && objeto) votar(voto,1,objeto); /* Votamos: en este tipo, el valor siempre será +1 */
});

Tenemos que tener en cuenta que podremos repetir estos elementos múltiples veces, razón por la cual deberemos tener siempre presente el intercambiar el ID del artículo o producto (de otro modo, se mostraría el mismo recuento para todos).

Un poco de CSS para poner bonitos nuestros dedos y un poco de javascript para hacer la votación:

.lkdlk {display:table;width:auto;margin:0 auto;}
    .lkdlk li {margin:0 10px;display:inline-block;width:auto;height:24px;transition:0.2s all linear;opacity:0.5}
    .lkdlk li:hover {opacity:1}
        .lkdlk .icon {display:block;float:left;width:24px;height:24px;background:url(../img/iconos.png) no-repeat}
            .like .icon {background-position:0 0}
            .dislike .icon {background-position:-24px 0}
           
            .lkdlk .count {position: relative;background: #909090;border-radius:5px;margin-left:10px;line-height:18px;padding:0 10px;font-size:10px;height:18px;display:inline-block;color:#FFF}
            .lkdlk .count:after {content: '';position: absolute;left: 0;top: 50%;width: 0;height: 0;border: 4px solid transparent;border-right-color: #909090;border-left: 0;margin-top: -4px;margin-left: -4px;}

Un fichero PHP con un pequeño código, recibirá nuestro voto y lo almacenará en la tabla correspondiente y asignándolo al producto o artículo en cuestión.

if (intval($_POST["puntaje"])>0 && intval($_POST["idarticulo"])>0)
{
    $idarticulo=intval($_POST["idarticulo"]);
    $tipo=mysqli_real_escape_string($db,$_POST["tipo"]); /* Evitamos inyección */
    $query=$db->query("update votos set votos=votos+1,puntaje=puntaje+".intval($_POST["puntaje"])." where tipo='".$tipo."' and idarticulo='".$idarticulo."' limit 1");
    if ($query)
    {
        if($db->affected_rows>0)
        {
            /* Voto realizado correctamente, debemos obtener nuevo valor para cambiarlo en el index */
            $querySelect=$db->query("select puntaje,votos from votos where tipo='".$tipo."' and idarticulo='".$idarticulo."' limit 1");
            if ($fila=$querySelect->fetch_array())
            {
                $votos=array("puntaje"=>$fila["puntaje"],"votos"=>$fila["votos"]);
            }
            else $votos=array();
            echo json_encode($votos);
        }
        else /* El registro no existe, entonces debemos crearlo */
        {
            $queryInsert=$db->query("insert into votos (idarticulo,tipo,puntaje,votos) values ('".$idarticulo."','".$tipo."','".intval($_POST["puntaje"])."',1)");
            if ($queryInsert)
            {
                /* Voto realizado correctamente, debemos obtener nuevo valor para cambiarlo en el index */
                $querySelect=$db->query("select puntaje,votos from votos where tipo='".$tipo."' and idarticulo='".$idarticulo."' limit 1");
                if ($fila=$querySelect->fetch_array())
                {
                    $votos=array("puntaje"=>$fila["puntaje"],"votos"=>$fila["votos"]);
                }
                else $votos=array();
                echo json_encode($votos);
            }
            else echo $db->error;
        }
    }
}

Adicionalmente, este código obtendrá el nuevo puntaje obtenido para devolverlo al javascript.

Nota importante: este código PHP se repite para todos los tipos de votación.

Votación con Estrellas

El típico star rating o votación con estrellas, donde asignamos a nuestro contenido una puntuación dentro del rango de 1 a 5, siendo 1 la puntuación más baja y 5 la más alta.

Este HTML será un poco más complejo, ya que debemos crear una capa superficial que seleccione la estrella a asignar, entonces tenemos:

Las estrellas base, es decir, 5 estrellas con borde pero sin relleno que hacen de base.

Las estrellas rellenas de color negro que indicarán el puntaje actual del contenido. En este caso, crearemos un div que estará posicionado por encima del base y su ancho será dinámico, resultando de la operación matemática:

Máximo posible = Total votos recibidos x 5 (ya que 5 es el puntaje máximo de cada voto)

Porcentaje del DIV = Puntaje Obtenido x 100 / Máximo posible

Para explicarnos mejor, si tenemos 3 votos (3 puntos, 4 puntos y 5 puntos) obtendremos:

Máximo posible = 3 x 5 (3 votos recibidos por el máximo de 5 puntos por voto) = 15 puntos

Porcentaje del DIV = (3+4+5) x 100 / 15 = 12 x 100 / 15 = 80

Este 80, sería igual a 80% ya que si el artículo o producto tuviera los 15 puntos, la capa superior (estrellas rellenas) deberían tener un ancho del 100% (es decir, las 5 estrellas pintadas); como nuestros votos suman 80, entendemos que es el 80% del tamaño del DIV base con lo que obtendremos 4 estrellas pintadas y 1 sin pintar.

Y por último, y por encima de estas dos capas, tendremos que crear una capa dinámica con 5 enlaces (cada uno llevará un valor de 1 a 5 como dato oculto) para que, al clicar sobre una estrella, el sistema entienda que quieres votar esa estrella y haga la correspondiente llamada Ajax a nuestro PHP que nos está esperando.

Básicamente tenemos:

<ul class="stars">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <?php
    $maximoposible=5*$votos["stars"]["votos"];
    if ($maximoposible>0)
    {
        $porcentaje=$votos["stars"]["puntaje"]*100/$maximoposible;
    }
    ?>
    <div class="stat" style="width:<?php echo $porcentaje; ?>%"><span></span><span></span><span></span><span></span><span></span></div>
    <div class="voto"><span><a href="#" data-voto="stars" data-value="5">5</a></span><span><a href="#" data-voto="stars" data-value="4">4</a></span><span><a href="#" data-voto="stars" data-value="3">3</a></span><span><a href="#" data-voto="stars" data-value="2">2</a></span><span><a href="#" data-voto="stars" data-value="1">1</a></span></div>
</ul>

En este tutorial es muy importante el atributo data-value ya que será el puntaje de la estrella pulsada. Tenemos que tener en cuenta que, por necesidades de la animación CSS al pasar el ratón por encima, el puntaje debe ir de mayor a menor ya que luego, por CSS, los elementos flotarán a la derecha, ordenándose.

Adornado con un poco de CSS:

.stars {display:table;width:100px;margin:0 auto;position:relative;overflow:hidden;height:20px}
    .stars li {display:inline-block;width:20px;height:20px;background:url(../img/iconos.png) 0 -24px no-repeat;margin:0;overflow:hidden;text-indent:-999em;}
    .stars:hover .voto {display:block}
    .stat {position:absolute;left:0;top:0;overflow:hidden;height:20px;z-index:101}
        .stat span {display:block;width:20px;height:20px;background:url(../img/iconos.png) no-repeat -20px -24px;position:absolute;top:0}
            .stat span:nth-child(2) {left:20px}
            .stat span:nth-child(3) {left:40px}
            .stat span:nth-child(4) {left:60px}
            .stat span:nth-child(5) {left:80px}
           
    .stars .voto {position:absolute;left:0;top:0;overflow:hidden;height:20px;z-index:102;display:none}
        .stars .voto span {display:block;width:20px;height:20px;background:url(../img/iconos.png) no-repeat 0 -24px;float:right;cursor:pointer;}
            .stars .voto span a {line-height:100px;display:block}
        .stars .voto span:hover,.voto span:hover ~ span {background:url(../img/iconos.png) no-repeat -20px -24px;}

Pero sin un poco de javascript de nada nos sirve:

$("ul.stars .voto a").click(function(e)
{
    e.preventDefault(); /* Evitamos el # en la barra de direcciones */
    var voto=$(this).data("voto"); /* Obtenemos el resultado del voto: like o dislike */
    var puntaje=$(this).data("value"); /* Obtenemos el resultado del voto: 1 a 5 */
    var objeto=$(this).closest("li").find(".count"); /* Obtenemos el elemento para cambiar el valor una vez realizado el voto */
    if (voto && objeto && puntaje) votar(voto,puntaje,objeto); /* Votamos: en este tipo, el valor siempre será la estrella elegida */
});

Y, por último, el código PHP para conectarnos a la base de datos y guardar ese voto (que es el mismo que puse más arriba).

Te doy mi corazón

Este sistema es mucho más sencillo ya que simplemente se limitará a decir que doy mi vida por lo que estoy viendo (como el like de Instagram).

Opcionalmente, podemos mostrar el recuento de corazones que llevamos recibidos (en el ejemplo lo hacemos).

Aquí, básicamente tenemos un código que muestra un corazón:

<ul class="hearts">
    <li class="heart"><a href="#" data-voto="heart"><span class="icon">&hearts;</span><span class="count"><?php echo ($votos["heart"]["puntaje"]?$votos["heart"]["puntaje"]:0); ?></span></a></li>
</ul>

Adornado con un poco de CSS:

.hearts {display:table;width:auto;margin:0 auto;}
    .hearts li {margin:0 10px;display:inline-block;width:auto;height:17px;transition:0.2s all linear;opacity:0.5}
    .hearts li:hover {opacity:1;}
    .hearts li:hover .icon {background-position:-20px -84px;}
    .hearts li:hover .count {color:#B20000}
    .hearts .icon {display:block;float:left;width:20px;height:17px;background:url(../img/iconos.png) 0 -84px no-repeat;text-indent:-999em;}
           
    .hearts .count {position: relative;color: #909090;line-height:13px;font-size:10px;height:13px;display:inline-block;margin-left:5px}

Y, como en los casos anteriores, un poco de javascript que hace la magia del voto:

$("ul.hearts li a").click(function(e)
{
    e.preventDefault(); /* Evitamos el # en la barra de direcciones */
    var voto=$(this).data("voto"); /* Obtenemos el resultado del voto: 1 */
    var objeto=$(this).closest("li").find(".count"); /* Obtenemos el elemento para cambiar el valor una vez realizado el voto */
    if (voto && objeto) votar(voto,1,objeto); /* Votamos: en este tipo, el valor siempre será +1 */
});

Y, también como en casos anteriores, el PHP que recibe el voto emitido.

Votación mediante emoticonos

Facebook fue el gran culpable de este sistema de votos… Emoticonos para expresar sentimientos y reacciones a los contenidos que leemos ya que, a veces, no es precisamente “Me gusta” lo que queremos decir.

Aquí, como no podía ser de otra manera, los hemos hecho un poco más gamberros que la conocida Red Social pero esto no quita que puedas personalizar tus emoticonos a tu gusto.

Empezaremos mostrando los emoticonos base y, como en otros casos, un recuento de las emociones recibidas en cada estado.

En este caso, nuestros estados serán: “LO AMO”, “ME GUSTA”, “SIN PALABRAS”, “WTF” y “LO ODIO” porque creo que representan más la forma en la que reaccionamos con lo que se lee hoy en día por la web.

Entonces, tenemos el siguiente HTML:

<ul class="emojis">
    <li><a href="#" data-voto="megusta"><span class="tooltip">&iexcl;ME GUSTA!</span><span class="icon em1"></span><span class="count"><?php echo ($votos["megusta"]["puntaje"]?$votos["megusta"]["puntaje"]:0); ?></span></a></li>
    <li><a href="#" data-voto="loamo"><span class="tooltip">&iexcl;LO AMO!</span><span class="icon em4"></span><span class="count"><?php echo ($votos["loamo"]["puntaje"]?$votos["loamo"]["puntaje"]:0); ?></span></a></li>
    <li><a href="#" data-voto="sinpalabras"><span class="tooltip">SIN PALABRAS</span><span class="icon em5"></span><span class="count"><?php echo ($votos["sinpalabras"]["puntaje"]?$votos["sinpalabras"]["puntaje"]:0); ?></span></a></li>
    <li><a href="#" data-voto="wtf"><span class="tooltip">&iexcl;WTF!</span><span class="icon em3"></span><span class="count"><?php echo ($votos["wtf"]["puntaje"]?$votos["wtf"]["puntaje"]:0); ?></span></a></li>
    <li><a href="#" data-voto="loodio"><span class="tooltip">&iexcl;LO ODIO!</span><span class="icon em2"></span><span class="count"><?php echo ($votos["loodio"]["puntaje"]?$votos["loodio"]["puntaje"]:0); ?></span></a></li>
</ul>

Nuevamente, es muy importante tener en cuenta el atributo data-voto ya que será el encargado de decirle al javascript el valor del voto emitido.

Al que personalizamos mediante CSS:

.emojis {display:table;width:auto;margin:0 auto;}
    .emojis li {margin:0 10px;display:inline-block;width:auto;height:20px;transition:0.2s all linear;opacity:0.5;position:relative}
    .emojis li:hover {opacity:1}
    .emojis li:hover .tooltip{opacity:1}
        .emojis .icon {display:block;float:left;width:20px;height:20px;background:url(../img/iconos.png) no-repeat}
            .emojis .em1 {background-position:0 -44px !important}
            .emojis .em2 {background-position:-20px -44px !important}
            .emojis .em3 {background-position:-40px -44px !important}
            .emojis .em4 {background-position:-60px -44px !important}
            .emojis .em5 {background-position:-80px -44px !important}
            .emojis li:hover .em1 {background-position:0 -64px !important}
            .emojis li:hover .em2 {background-position:-20px -64px !important}
            .emojis li:hover .em3 {background-position:-40px -64px !important}
            .emojis li:hover .em4 {background-position:-60px -64px !important}
            .emojis li:hover .em5 {background-position:-80px -64px !important}
           
            .emojis .count {position: relative;background: #909090;border-radius:5px;margin-left:10px;line-height:18px;padding:0 10px;font-size:10px;height:18px;display:inline-block;color:#FFF}
                .emojis .count:after {content: '';position: absolute;left: 0;top: 50%;width: 0;height: 0;border: 4px solid transparent;border-right-color: #909090;border-left: 0;margin-top: -4px;margin-left: -4px;}
               
            .emojis .tooltip {position: absolute;background: #909090;border-radius:5px;padding:5px 10px;font-size:10px;color:#FFF;opacity:0;top:25px;white-space:pre;transform: translate(-50%, 0);font-family:sans-serif}
                .emojis .tooltip:after {content: '';position: absolute;left:50%;top:-4px;width: 0;height: 0;border: 4px solid transparent;border-bottom-color: #909090;border-top: 0;}

Pero no nos valdría de nada sin este pequeño Javascript:

$("ul.emojis li a").click(function(e)
{
    e.preventDefault(); /* Evitamos el # en la barra de direcciones */
    var voto=$(this).data("voto"); /* Obtenemos el resultado del voto: 1 */
    var objeto=$(this).closest("li").find(".count"); /* Obtenemos el elemento para cambiar el valor una vez realizado el voto */
    if (voto && objeto) votar(voto,1,objeto); /* Votamos: en este tipo, el valor siempre será +1 */
});

Y, como siempre, el PHP se ocupará de la tarea pesada de almacenar el voto.

Códigos genéricos:

El sistema usa una serie de códigos genéricos que son:

  1. Carga de votos iniciales mediante PHP
$db = new mysqli(DBHOST,DBUSER,DBPASS,DBNAME);
if ($db->connect_errno)
{
    die ("<h1>Fallo al conectar a MySQL: (" . $db->connect_errno . ") " . $db->connect_error."</h1>");
}
else
{
    $query=$db->query("select tipo,puntaje,votos from votos where idarticulo='1'");
    if ($fila=$query->fetch_array())
    {
        $votos=array();
        do
        {
            $clave=$fila["tipo"];
            $votos[$clave]=array("puntaje"=>$fila["puntaje"],"votos"=>$fila["votos"]);
        }
        while($fila=$query->fetch_array());
    }
    else $votos=array();
}

Esto nos permitirá tener en el array votos[tipo_de_voto] la cantidad obtenida hasta el momento de cargar la página inicialmente.

  1. Envío de votos al fichero PHP mediante código javascript (damos por hecho que necesitas incluir la librería jQuery).
function votar(tipo,puntaje,objeto)
{
    $.ajax({
        method: "POST",
        url: "ajax.php",
        dataType: 'json',
        cache: false,
        data: { accion:"vote",tipo:tipo,puntaje:puntaje,idarticulo:1 }
    })
    .done(function(msg)
    {
        if (tipo!="stars") /* si el tipo de voto no es de estrellas, entonces mostramos el nuevo valor obtenido */
        {
            objeto.html(msg.votos);
        }
        else if (tipo=="stars") /* al ser voto de estrella, hay que recalcular la capa que está por encima */
        {
            var maximoposible=msg.votos*5;
            var porcentaje=msg.puntaje*100/maximoposible;
            $(".stars .stat").css("width",porcentaje+"%");
        }
        alerta("Gracias por tu voto :)","ok");
    })
    .fail(function(msg) {
        alerta(msg,"ko");
    });
}
  1. Función “alerta” hemos dicho…
function alerta(mensaje,modo)
{
    $("body").append("<div class='mensaje "+modo+"'>"+mensaje+"</div>");
    $('.mensaje').fadeOut(4000, function(){ $(this).remove(); });
}

.mensaje {position:fixed;left:25px;bottom:25px;max-width:250px;padding:10px;border-radius:4px;font-size:16px;text-shadow:0 0 1px #000;color:#FFF}
    .ok {background:green}
    .ko {background:red}

Para todos los ejemplos, nos hemos basado en este sprite creado en formato PNG que contiene todas las imágenes necesarias.

Pues bien, hasta aquí hemos llegado con este mini tutorial. Creo que estos 4 métodos cubrirán todas tus necesidades y, como siempre digo, si lo mejoras me encantará saber qué le has hecho.

Para evitar múltiples votos

Como dije antes, el sistema no cubre estas opciones pero podemos bloquear múltiples votos de diferentes maneras tales como:

  • Generando una cookie (cuidado con el RGPD, si te cambian el navegador te engañan, al menos 1 vez más)
  • Capturando la IP del visitante (método menos fiable ya que la IP puede cambiar)
  • Bloqueando el voto mediante una $_SESSION en PHP que dure X tiempo

Y no se me ocurre algún método más por ahora pero realmente te tienes que preguntar si te vale la pena el esfuerzo de fortalecer un código que, en el 90% de las veces, es irrelevante en una web y está de adorno o porque “mola”.

Pues nada, me despido, un vez más agradeciendo a Félix el contar conmigo otro año más y, si este tutorial te hizo ahorrar al menos 5 minutos de tu tiempo, me auto-voto con un corazón, un lo amo, un pulgar arriba y las 5 estrellas 🙂