Calificacion por estrellas con jQuery, AJAX y PHP

Actualizado el domingo, 19 junio, 2016

Implantaremos un sistema de calificación completo. Para eso, vamos a manejar la interfaz con jQuery y CSS y administrar el backend con PHP. Para facilitar las cosas, la calificación se actualizará en tiempo real mediante AJAX y los datos se almacenarán en un archivo de texto. En este ejemplo vamos a poder calificar dos películas: Despicable Me 2 y Monster University. Para mantener el orden, todo el código jQuery va a estar en un archivo llamado script.js, mientras que todo el código CSS va a estar almacenado en un archivo llamado styles.css.

Para cada película creamos un div con clase divPelicula como contenedor. Dentro de este, hay dos divs más: uno para la portada y otro para la información de los votos. Dentro del de información, encontramos otro div que va a contener todos los datos de puntaje de la película. Cada divValoracion, debe contener un id específico para saber de qué película se trata (lo usaremos al momento de la votación). Para cada estrella creamos un div, al cual le establecemos la clase estrellasValoracion y una clase llamada estrella_X, donde X representa un número de 1 al 5. Este número nos va a permitir saber por cuál estrella se votó. Finalmente dentro del divValoración, tenemos otro div con clase votosTotales donde mostraremos el puntaje exacto de cada película.

<!doctype html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Nebaris - Valoración películas</title>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
    <script src="script.js"></script>
    <link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Películas en el cine</h1>
<div class='divPelicula'>
    <div class="divPortada">
        <img src="despicableme2.jpg" alt="Despicable Me 2">
    </div>
    <div class="divInfo">
        <h3>Despicable Me 2</h3>
        <div id="dm2" class="divValoracion">
            <div class="estrella_1 estrellasValoracion"></div>
            <div class="estrella_2 estrellasValoracion"></div>
            <div class="estrella_3 estrellasValoracion"></div>
            <div class="estrella_4 estrellasValoracion"></div>
            <div class="estrella_5 estrellasValoracion"></div>
            <div class="votosTotales">Datos de votación</div>
        </div>
    </div>
</div>

<div class='divPelicula'>
    <div class="divPortada">
        <img src="monsteruniversity.jpg" alt="Monster University">
    </div>
    <div class="divInfo">
        <h3>Monsters University</h3>
        <div id="mu" class="divValoracion">
            <div class="estrella_1 estrellasValoracion"></div>
            <div class="estrella_2 estrellasValoracion"></div>
            <div class="estrella_3 estrellasValoracion"></div>
            <div class="estrella_4 estrellasValoracion"></div>
            <div class="estrella_5 estrellasValoracion"></div>
            <div class="votosTotales">Datos de votación</div>
        </div>
    </div>
</div>
</body>
</html>

Comenzamos a editar el documento, primero vamos por las cabeceras.
h1 {
    color: #333;
    font-size: 21px;
    line-height: 110%;
    text-align: center;
    font-family: Verdana,Arial,sans-serif;
}

h3 {
    font-family: Verdana,Arial,sans-serif;
    color: #136cb2;
    font-size: 13px;
}

El div que contiene todos los datos de la película.

.divPelicula {
    font: 10px verdana, sans-serif;
    margin: 0 auto 40px auto;
    width: 400px;
    background-color: #f6f6f5;
    border: #fff 1px solid;
    clear: both;
    overflow: auto;
    padding: 10px;
}

A los dos divs hijos, les indicamos el float por izquierda.

.divPortada,
.divInfo{
    float: left;
}

Editamos el div de la carátula

.divPortada{
    margin-right: 10px;
}

El contenedor de las estrellas y el puntaje.

.divValoracion {
    border:     1px solid #CCC;
    overflow:   visible;
    padding:    10px;
    position:   relative;
    width:      140px;
    height:     24px;
    background-color: white;
    border-radius: 5px;
}

.divValoracion:hover{
    border:     1px solid #666;
}

Empezamos a trabajar con las estrellas. Indicamos que en principio las mismas estén vacías (imagen de una estrella vacía).

.estrellasValoracion {
    background: url('estrellaVacia.png') no-repeat;
    float:      left;
    height:     24px;
    padding:    2px;
    width:      24px;
}

Cuando obtengamos los votos, le incluiremos esta clase.

.estrellaValoracion {
    background: url('estrellaLlena.png') no-repeat;
}

Y cuando se ponga el mouse por encima, le agregaremos la siguiente clase, para diferenciar las estrellas que ya posee, de las que nosotros queremos otorgarle.

.estrellaVotar {
    background: url('estrellaHover.png') no-repeat;
}

Para terminar, le damos estilo al div de la información de los votos.

.votosTotales {
    background: #eaeaea;
    top: 58px;
    left: 0;
    padding: 5px;
    position:   absolute;  
}

jQuery

Nuestro código tiene que hacer varias cosas. Por lo que vamos a ir paso a paso para no perdernos. Primero y principal, vamos a indicar que el resto del código debe ejecutarse cuando la página se haya cargado correctamente.

$(document).ready(function() {
///  
Necesitamos saber cuántos puntos tiene cada película, para poder indicarlo con estrellas. Para eso, vamos a llamar al archivo PHP mediante AJAX, utilizando un objeto JSON.


Obtenemos todos los divs marcados con la clase divValoración
e iteractuamosmos entre ellos
///$('.divValoracion').each(function() {
    var pelicula = this;

   #creamos el objeto JSON que enviaremos a la página PHP
    var datos = {
        pelicula_id : $(pelicula).attr('id'), // este es el id que mencionamos antes  
        buscar: 1 // indicamos que queremos obtener la información y no guardar un voto
    };

    #cargamos datos del servidor utilizando un pedido HTTP POST
    $.post(
        'ratings.php', // archivo que va a recibir nuestro pedido                    
        datos, // el objeto que creamos antes

        // función que se ejecutará cuando obtengamos la respuesta
        function(INFO) { // INFO son los datos que nos devuelve la página PHP

            // data es un método jQuery que nos permite asociar datos a un objeto del DOM
            $(pelicula).data( 'puntaje', INFO );

            // llamamos al método que carga los valores en las estrellas y la información
            indicarVotos(pelicula);
        },
        'json' // indicamos que el formato utilizado es JSON
    );
});

Lo siguiente, es darle interacción a las estrellas; para que al pasar el mouse, se cambien las imágenes de acuerdo a dónde está puesto el mouse.
// cuando pasamos el mouse por encima de las estrellas
$('.estrellasValoracion').hover(

    // la función hover necesita que definamos dos funciones
    // una para cuando el puntero del mouse se posiciona sobre el elemento
    function() {

        // dependiendo el div en el que nos encontremos,
        // a todos los divs anteriores y al que tiene el mouse encima, 
        // les agregamos esta clase
        $(this).prevAll().andSelf().addClass('estrellaVotar');

        // a los siguientes les quitamos esta clase
        $(this).nextAll().removeClass('estrellaValoracion'); 
    },

    // y una para cuando el mouse deja el elemento
    function() {

        // a todos los divs anteriores y al que tenía el mouse encima, les quitamos esta clase
        $(this).prevAll().andSelf().removeClass('estrellaVotar');

        // llamamos al método para volver a cargar el valor original
        indicarVotos($(this).parent());
    }
);

Necesitamos guardar el voto del usuario, al momento de hacer click sobre una de las estrellas.
// este método es el que guarda el voto, al hacer click sobre una estrella
$('.estrellasValoracion').bind('click', function() {

    // obtenemos la estrella sobre la que se hizo click
    var estrella = this;

    // obtenemos la película a la que pertenece la estrella,
    // esto nos da el div que tiene el id
    var pelicula = $(this).parent(); 

    // creamos el objeto JSON para enviar a la página PHP
    // esta vez, como queremos votar, no le enviamos la propiedad "buscar"
    var datosClick = {
        clickEstrella : $(estrella).attr('class'), // le pasamos la clase que indica el número de estrella
        pelicula_id : $(estrella).parent().attr('id')
    };

    // cargamos datos del servidor utilizando la misma llamada de antes
    $.post(
        'ratings.php', 
        datosClick, 

        function(INFO) { 
            pelicula.data( 'puntaje', INFO );
            indicarVotos(pelicula);
        },
        'json'
    ); 
});

Por último, nos falta cargar las estrellas con los datos obtenidos en la página PHP.
function indicarVotos(pelicula) {

    // extraemos la información guardada en el objeto DOM donde está la película
    // y creamos 3 variables para mostrar los datos
    var promedioRedondeado = $(pelicula).data('puntaje').promedioRedondeado;
    var votos = $(pelicula).data('puntaje').numeroDeVotos;
    var promedioExacto = $(pelicula).data('puntaje').promedioExacto;            

    // buscamos la estrella de la película que tenga el número igual al promedio redondeado
    // de esa para atrás, les cargamos la clase estrellaValoración
    $(pelicula).find('.estrella_' + promedioRedondeado).prevAll().andSelf().addClass('estrellaValoracion');

    // a las que la suceden (si las hubiera) les quitamos la clase (por si la tenían) para que queden vacías
    $(pelicula).find('.estrella_' + promedioRedondeado).nextAll().removeClass('estrellaValoracion'); 

    // mostramos la cantidad de votos y el promedio exacto
    $(pelicula).find('.votosTotales').text( votos + ' votos (' + promedioExacto + ')' );
}

PHP Nuestro archivo PHP va a contar con dos cosas. Por un lado vamos a crear una clase que administre los puntajes, y por el otro una validación para saber si al momento de acceder a la página se quiere buscar los votos o votar. Empecemos con la clase. Va a contener 3 propiedades: el archivo de texto donde guardamos los votos, el id de la película que se necesita y un arreglo donde mantendremos los datos de la misma.
// definición de la clase
class ratings {

    // el archivo donde guardamos los datos de los votos
    var $data_file = './ratings.data.txt';
    private $pelicula_id;
    private $data = array();

Le creamos un constructor para establecer los datos al inicio.
// el constructor de la clase va a recibir la película
function __construct($peli) {

    // guardamos la película en la propiedad
    $this->pelicula_id = $peli;

    // file_get_contents devuelve lo que está en el archivo de texto a una variable string
    $info = file_get_contents($this->data_file);

    // si se cargó el archivo
    if($info) {

        // transformamos los datos planos a un array en php
        $this->data = unserialize($info);
    }
}

Hasta ahora, cada vez que se cree un objeto de esta clase, le asignaremos el id de la película y va a intentar levantar los datos desde el archivo de texto. Nos falta crear dos métodos, uno para obtener los votos de una película.
public function obtenerRating() {

    // si en el arreglo con los datos del archivo txt (que se cargó en el constructor)
    // está el id de la película
    if($this->data[$this->pelicula_id]) {

        // devolvemos los datos de la película en formato JSON a la página
        echo json_encode($this->data[$this->pelicula_id]);
    }
    else { // caso contrario

        // cargamos los datos de la película al arreglo, con los valores por defecto
        $data['pelicula_id'] = $this->pelicula_id;
        $data['numeroDeVotos'] = 0;
        $data['votosTotales'] = 0;
        $data['promedioExacto'] = 0;
        $data['promedioRedondeado'] = 0;

        // devolvemos el objeto recién creado en formato JSON a la página
        echo json_encode($data);
    } 
}

Y otro para votar…
public function votar() {

    // necesitamos saber qué estrella es la que se votó
    // para eso, usamos preg_match, que realiza una comparación 
    // tomando la expresión regular, la cadena de entrada 
    // (la estrella en la que hizo click) y dejando el resultado en $resultado
    preg_match('/estrella_([1-5]{1})/', $_POST['clickEstrella'], $resultado);

    // guardamos el valor de la estrella
    $votar = $resultado[1];

    $ID = $this->pelicula_id;

    // si existe la película en el arreglo (cargado en el constructor)
    if($this->data[$ID]) {

        // aumentamos el número de votos en 1
        $this->data[$ID]['numeroDeVotos'] += 1;

        // sumamos el voto a los votos totales
        $this->data[$ID]['votosTotales'] += $votar;
    }
    else { // si no existe la película

        // indicamos que es el primer voto
        $this->data[$ID]['numeroDeVotos'] = 1;

        // indicamos el número del primer voto
        $this->data[$ID]['votosTotales'] = $votar;
    }

    // calculamos el promedio exacto
    $this->data[$ID]['promedioExacto'] = 
        round( $this->data[$ID]['votosTotales'] / $this->data[$ID]['numeroDeVotos'], 1 );

    // redondeamos el promedio, para no tener que volver a hacerlo en la página
    $this->data[$ID]['promedioRedondeado'] = round( $this->data[$ID]['promedioExacto'] );

    // guardamos el arreglo en formato plano de nuevo en el archivo de texto
    file_put_contents($this->data_file, serialize($this->data));

    // obtenemos el nuevo rating para enviárselo a la página
    $this->obtenerRating();
}

Solo falta controlar las llamadas que vendrán por JSON desde la página. Para eso, sólo necesitamos dos líneas de código.

// se crea un nuevo objeto y le pasamos el id de la película que vino por el postback
$rating = new ratings($_POST['pelicula_id']);

// si en el postback se indicó la variable buscar, obtenemos los ratings, 
// sino guardamos el voto
isset($_POST['buscar']) ? $rating->obtenerRating() : $rating->votar();

 

4 Comentarios

  1. Federico Castañeda
    12/05/2017
  2. Fernando
    29/10/2017
    • sergio
      29/10/2017
  3. David Medina
    09/05/2018

Agregar comentario