[MU] Sistema de Votação com Estrelas

Aula: Sistema de Votação com Estrelas (Rating)

Objetivo: Criar um MU-Plugin leve que adiciona um sistema de votação de 1 a 5 estrelas em posts, páginas ou custom post types, substituindo completamente o plugin wp-postratings e similares.

Contexto: O plugin original permite que visitantes avaliem posts com estrelas, armazenando a média e o total de votos. Nosso MU-Plugin implementa as mesmas funcionalidades, com código limpo, AJAX e prevenção de votos duplicados (por IP + cookie).

Funcionalidades implementadas

  • Exibição de estrelas (1 a 5) ao lado da média atual.
  • Votação via AJAX (não recarrega a página).
  • Armazenamento das notas como metadados do post (_rating_average, _rating_count, _rating_total).
  • Prevenção de múltiplos votos por IP + cookie (expira após 24 horas – ajustável).
  • Shortcode [ratings] para exibir o sistema dentro do conteúdo.
  • Função que retorna o rating formatado (estrelas em HTML/CSS).
  • Compatível com tema Hello Elementor e qualquer tema que respeite o loop do WordPress.

Código do MU-Plugin: neodoor-ratings.php

Crie a pasta wp-content/mu-plugins/neodoor-ratings/ e dentro dela o arquivo neodoor-ratings.php com o conteúdo abaixo.

<?php
/**
 * Plugin Name:  Neodoor Ratings (WP-PostRatings substitute)
 * Description:  Sistema de votação com estrelas (1 a 5) via AJAX, com prevenção de múltiplos votos.
 * Version:      1.0
 * Author:       Reinaldo (Neodoor.neo)
 */

if ( ! defined( 'ABSPATH' ) ) exit;

class Neodoor_Ratings {
    private static $instance = null;

    public static function get_instance() {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        add_shortcode( 'ratings', [ $this, 'ratings_shortcode' ] );
        add_action( 'wp_ajax_neodoor_rate_post', [ $this, 'ajax_rate_post' ] );
        add_action( 'wp_ajax_nopriv_neodoor_rate_post', [ $this, 'ajax_rate_post' ] );
        add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
    }

    public function enqueue_scripts() {
        if ( ! is_singular() ) return;
        wp_enqueue_script( 'neodoor-ratings', '', [], '1.0', true );
        wp_add_inline_script( 'neodoor-ratings', $this->inline_js() );
        wp_localize_script( 'neodoor-ratings', 'neodoor_ajax', [
            'ajax_url' => admin_url( 'admin-ajax.php' ),
            'nonce'    => wp_create_nonce( 'neodoor_rating_nonce' ),
        ] );
    }

    private function inline_js() {
        return <<<JS
        document.addEventListener('DOMContentLoaded', function() {
            document.querySelectorAll('.neodoor-rating-star').forEach(star => {
                star.addEventListener('click', function(e) {
                    e.preventDefault();
                    const postId = this.dataset.postId;
                    const rating = this.dataset.rating;
                    const starContainer = this.closest('.neodoor-ratings-container');
                    if (starContainer.classList.contains('voted')) return;

                    fetch(neodoor_ajax.ajax_url, {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                        body: new URLSearchParams({
                            action: 'neodoor_rate_post',
                            post_id: postId,
                            rating: rating,
                            nonce: neodoor_ajax.nonce
                        })
                    })
                    .then(response => response.json())
                    .then(data => {
                        if (data.success) {
                            starContainer.classList.add('voted');
                            starContainer.innerHTML = data.data.html;
                        } else {
                            alert(data.data.message);
                        }
                    });
                });
            });
        });
        JS;
    }

    public function ratings_shortcode( $atts ) {
        $post_id = get_the_ID();
        if ( ! $post_id ) return '';
        return $this->render_ratings( $post_id );
    }

    private function render_ratings( $post_id ) {
        $avg = get_post_meta( $post_id, '_rating_average', true );
        $count = get_post_meta( $post_id, '_rating_count', true );
        if ( ! $avg ) $avg = 0;
        if ( ! $count ) $count = 0;

        $stars_html = '';
        for ( $i = 1; $i <= 5; $i++ ) {
            $class = $i <= round($avg) ? 'star-filled' : 'star-empty';
            $stars_html .= '<span class="neodoor-rating-star ' . $class . '" data-post-id="' . $post_id . '" data-rating="' . $i . '">★</span>';
        }

        $voted_class = $this->has_user_voted( $post_id ) ? 'voted' : '';

        return '<div class="neodoor-ratings-container ' . $voted_class . '" data-post-id="' . $post_id . '">
            <div class="neodoor-stars">' . $stars_html . '</div>
            <div class="neodoor-rating-text">' . number_format( $avg, 1 ) . ' (' . $count . ' votos)</div>
        </div>';
    }

    private function has_user_voted( $post_id ) {
        $ip = $_SERVER['REMOTE_ADDR'];
        $voted = get_post_meta( $post_id, '_rating_voted_ips', true );
        if ( ! is_array( $voted ) ) $voted = [];
        $cookie = isset( $_COOKIE['neodoor_rated_' . $post_id] ) ? true : false;
        return in_array( $ip, $voted ) || $cookie;
    }

    public function ajax_rate_post() {
        check_ajax_referer( 'neodoor_rating_nonce', 'nonce' );
        $post_id = intval( $_POST['post_id'] );
        $rating  = intval( $_POST['rating'] );
        if ( $rating < 1 || $rating > 5 ) wp_send_json_error( [ 'message' => 'Nota inválida.' ] );
        if ( $this->has_user_voted( $post_id ) ) wp_send_json_error( [ 'message' => 'Você já votou neste conteúdo.' ] );

        // Atualiza métricas
        $total = get_post_meta( $post_id, '_rating_total', true );
        $count = get_post_meta( $post_id, '_rating_count', true );
        $total = $total ? $total : 0;
        $count = $count ? $count : 0;
        $total += $rating;
        $count++;
        $new_avg = $total / $count;

        update_post_meta( $post_id, '_rating_average', $new_avg );
        update_post_meta( $post_id, '_rating_count', $count );
        update_post_meta( $post_id, '_rating_total', $total );

        // Registra voto por IP
        $voted_ips = get_post_meta( $post_id, '_rating_voted_ips', true );
        if ( ! is_array( $voted_ips ) ) $voted_ips = [];
        $voted_ips[] = $_SERVER['REMOTE_ADDR'];
        update_post_meta( $post_id, '_rating_voted_ips', $voted_ips );

        // Cookie de 24h
        setcookie( 'neodoor_rated_' . $post_id, '1', time() + 86400, COOKIEPATH, COOKIE_DOMAIN );

        wp_send_json_success( [ 'html' => $this->render_ratings( $post_id ), 'message' => 'Voto computado!' ] );
    }
}

Neodoor_Ratings::get_instance();

Estilo CSS sugerido (adicione no tema ou no MU-Plugin de CSS):

<style>
.neodoor-ratings-container {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 15px 0;
}
.neodoor-stars {
    display: inline-flex;
    gap: 4px;
}
.neodoor-rating-star {
    font-size: 24px;
    cursor: pointer;
    color: #ddd;
    transition: color 0.2s;
}
.neodoor-rating-star.star-filled {
    color: #ffc107;
}
.neodoor-ratings-container.voted .neodoor-rating-star {
    cursor: default;
    pointer-events: none;
}
.neodoor-rating-text {
    font-size: 14px;
    color: #555;
}
</style>

Instruções de instalação

  1. Crie a pasta neodoor-ratings dentro de /wp-content/mu-plugins/.
  2. Dentro dela, crie o arquivo neodoor-ratings.php com o código acima (e adicione o CSS no seu tema ou no MU-Plugin de estilos).
  3. No conteúdo de posts/páginas onde quiser exibir as estrelas, insira o shortcode [ratings].
  4. Desative e remova o plugin wp-postratings e outros similares.

Como testar

  • Acesse um post ou página onde o shortcode foi inserido.
  • Clique em uma estrela (qualquer uma, de 1 a 5).
  • Após o clique, a média e o total de votos serão atualizados via AJAX, sem recarregar a página.
  • Tente votar novamente (mesmo IP/mesmo cookie) – o sistema deve bloquear e exibir mensagem.
  • Se necessário, limpe os cookies ou use um navegador anônimo para testar novo voto.

Personalizações avançadas

  • Alterar tempo do cookie: modifique 86400 (segundos) para outro valor (ex: 604800 = 7 dias).
  • Remover bloqueio por IP: comente as linhas que usam _rating_voted_ips.
  • Estrelas com imagem ou ícone: troque o texto por imagens ou use Font Awesome.
  • Exibir apenas em certos CPTs: no shortcode, verifique get_post_type().

Plugins substituídos

  • wp-postratings – totalmente substituído.
  • Qualquer outro plugin de avaliação por estrelas similar.

Observações finais

  • O sistema armazena três metadados por post: _rating_average, _rating_count, _rating_total e um array de IPs _rating_voted_ips. Em sites muito visitados, esse array pode crescer; recomenda-se limpar ou usar apenas cookies.
  • O bloqueio por IP + cookie não é 100% à prova de fraude, mas é suficiente para a maioria dos sites.
  • Se você usa cache (WP Super Cache, etc.), o AJAX ainda funciona porque a requisição é feita diretamente ao admin-ajax.php, não à página cacheada.

Registro no caderno

Data:

Funcionalidade: Sistema de votação com estrelas (1 a 5) via AJAX.

Plugins substituídos: wp-postratings e similares.

Tempo de implementação: ~15 minutos.

✅ Voto computado! Obrigado.

Média: 3.1 (20 votos)