{"id":328,"date":"2026-05-13T20:00:48","date_gmt":"2026-05-13T23:00:48","guid":{"rendered":"https:\/\/neodoor.neo\/?post_type=neodoor_aula&#038;p=328"},"modified":"2026-05-21T09:05:10","modified_gmt":"2026-05-21T12:05:10","slug":"sistema-de-votacao","status":"publish","type":"post","link":"https:\/\/www.portanova.com.br\/notebook\/sistema-de-votacao\/","title":{"rendered":"[MU] Sistema de Vota\u00e7\u00e3o com Estrelas"},"content":{"rendered":"<h2>Aula: Sistema de Vota\u00e7\u00e3o com Estrelas (Rating)<\/h2>\n<p><strong>Objetivo:<\/strong> Criar um MU-Plugin leve que adiciona um sistema de vota\u00e7\u00e3o de 1 a 5 estrelas em posts, p\u00e1ginas ou custom post types, substituindo completamente o plugin <code>wp-postratings<\/code> e similares.<\/p>\n<p><strong>Contexto:<\/strong> O plugin original permite que visitantes avaliem posts com estrelas, armazenando a m\u00e9dia e o total de votos. Nosso MU-Plugin implementa as mesmas funcionalidades, com c\u00f3digo limpo, AJAX e preven\u00e7\u00e3o de votos duplicados (por IP + cookie).<\/p>\n<h3>Funcionalidades implementadas<\/h3>\n<ul>\n<li>Exibi\u00e7\u00e3o de estrelas (1 a 5) ao lado da m\u00e9dia atual.<\/li>\n<li>Vota\u00e7\u00e3o via AJAX (n\u00e3o recarrega a p\u00e1gina).<\/li>\n<li>Armazenamento das notas como metadados do post (<code>_rating_average<\/code>, <code>_rating_count<\/code>, <code>_rating_total<\/code>).<\/li>\n<li>Preven\u00e7\u00e3o de m\u00faltiplos votos por IP + cookie (expira ap\u00f3s 24 horas \u2013 ajust\u00e1vel).<\/li>\n<li>Shortcode <code>[ratings]<\/code> para exibir o sistema dentro do conte\u00fado.<\/li>\n<li>Fun\u00e7\u00e3o que retorna o rating formatado (estrelas em HTML\/CSS).<\/li>\n<li>Compat\u00edvel com tema Hello Elementor e qualquer tema que respeite o loop do WordPress.<\/li>\n<\/ul>\n<h3>C\u00f3digo do MU-Plugin: <code>neodoor-ratings.php<\/code><\/h3>\n<p>Crie a pasta <code>wp-content\/mu-plugins\/neodoor-ratings\/<\/code> e dentro dela o arquivo <code>neodoor-ratings.php<\/code> com o conte\u00fado abaixo.<\/p>\n<pre><code>&lt;?php\n\/**\n * Plugin Name:  Neodoor Ratings (WP-PostRatings substitute)\n * Description:  Sistema de vota\u00e7\u00e3o com estrelas (1 a 5) via AJAX, com preven\u00e7\u00e3o de m\u00faltiplos votos.\n * Version:      1.0\n * Author:       Reinaldo (Neodoor.neo)\n *\/\n\nif ( ! defined( 'ABSPATH' ) ) exit;\n\nclass Neodoor_Ratings {\n    private static $instance = null;\n\n    public static function get_instance() {\n        if ( null === self::$instance ) {\n            self::$instance = new self();\n        }\n        return self::$instance;\n    }\n\n    private function __construct() {\n        add_shortcode( 'ratings', [ $this, 'ratings_shortcode' ] );\n        add_action( 'wp_ajax_neodoor_rate_post', [ $this, 'ajax_rate_post' ] );\n        add_action( 'wp_ajax_nopriv_neodoor_rate_post', [ $this, 'ajax_rate_post' ] );\n        add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_scripts' ] );\n    }\n\n    public function enqueue_scripts() {\n        if ( ! is_singular() ) return;\n        wp_enqueue_script( 'neodoor-ratings', '', [], '1.0', true );\n        wp_add_inline_script( 'neodoor-ratings', $this-&gt;inline_js() );\n        wp_localize_script( 'neodoor-ratings', 'neodoor_ajax', [\n            'ajax_url' =&gt; admin_url( 'admin-ajax.php' ),\n            'nonce'    =&gt; wp_create_nonce( 'neodoor_rating_nonce' ),\n        ] );\n    }\n\n    private function inline_js() {\n        return &lt;&lt;&lt;JS\n        document.addEventListener('DOMContentLoaded', function() {\n            document.querySelectorAll('.neodoor-rating-star').forEach(star =&gt; {\n                star.addEventListener('click', function(e) {\n                    e.preventDefault();\n                    const postId = this.dataset.postId;\n                    const rating = this.dataset.rating;\n                    const starContainer = this.closest('.neodoor-ratings-container');\n                    if (starContainer.classList.contains('voted')) return;\n\n                    fetch(neodoor_ajax.ajax_url, {\n                        method: 'POST',\n                        headers: { 'Content-Type': 'application\/x-www-form-urlencoded' },\n                        body: new URLSearchParams({\n                            action: 'neodoor_rate_post',\n                            post_id: postId,\n                            rating: rating,\n                            nonce: neodoor_ajax.nonce\n                        })\n                    })\n                    .then(response =&gt; response.json())\n                    .then(data =&gt; {\n                        if (data.success) {\n                            starContainer.classList.add('voted');\n                            starContainer.innerHTML = data.data.html;\n                        } else {\n                            alert(data.data.message);\n                        }\n                    });\n                });\n            });\n        });\n        JS;\n    }\n\n    public function ratings_shortcode( $atts ) {\n        $post_id = get_the_ID();\n        if ( ! $post_id ) return '';\n        return $this-&gt;render_ratings( $post_id );\n    }\n\n    private function render_ratings( $post_id ) {\n        $avg = get_post_meta( $post_id, '_rating_average', true );\n        $count = get_post_meta( $post_id, '_rating_count', true );\n        if ( ! $avg ) $avg = 0;\n        if ( ! $count ) $count = 0;\n\n        $stars_html = '';\n        for ( $i = 1; $i &lt;= 5; $i++ ) {\n            $class = $i &lt;= round($avg) ? 'star-filled' : 'star-empty';\n            $stars_html .= '&lt;span class=\"neodoor-rating-star ' . $class . '\" data-post-id=\"' . $post_id . '\" data-rating=\"' . $i . '\"&gt;\u2605&lt;\/span&gt;';\n        }\n\n        $voted_class = $this-&gt;has_user_voted( $post_id ) ? 'voted' : '';\n\n        return '&lt;div class=\"neodoor-ratings-container ' . $voted_class . '\" data-post-id=\"' . $post_id . '\"&gt;\n            &lt;div class=\"neodoor-stars\"&gt;' . $stars_html . '&lt;\/div&gt;\n            &lt;div class=\"neodoor-rating-text\"&gt;' . number_format( $avg, 1 ) . ' (' . $count . ' votos)&lt;\/div&gt;\n        &lt;\/div&gt;';\n    }\n\n    private function has_user_voted( $post_id ) {\n        $ip = $_SERVER['REMOTE_ADDR'];\n        $voted = get_post_meta( $post_id, '_rating_voted_ips', true );\n        if ( ! is_array( $voted ) ) $voted = [];\n        $cookie = isset( $_COOKIE['neodoor_rated_' . $post_id] ) ? true : false;\n        return in_array( $ip, $voted ) || $cookie;\n    }\n\n    public function ajax_rate_post() {\n        check_ajax_referer( 'neodoor_rating_nonce', 'nonce' );\n        $post_id = intval( $_POST['post_id'] );\n        $rating  = intval( $_POST['rating'] );\n        if ( $rating &lt; 1 || $rating &gt; 5 ) wp_send_json_error( [ 'message' =&gt; 'Nota inv\u00e1lida.' ] );\n        if ( $this-&gt;has_user_voted( $post_id ) ) wp_send_json_error( [ 'message' =&gt; 'Voc\u00ea j\u00e1 votou neste conte\u00fado.' ] );\n\n        \/\/ Atualiza m\u00e9tricas\n        $total = get_post_meta( $post_id, '_rating_total', true );\n        $count = get_post_meta( $post_id, '_rating_count', true );\n        $total = $total ? $total : 0;\n        $count = $count ? $count : 0;\n        $total += $rating;\n        $count++;\n        $new_avg = $total \/ $count;\n\n        update_post_meta( $post_id, '_rating_average', $new_avg );\n        update_post_meta( $post_id, '_rating_count', $count );\n        update_post_meta( $post_id, '_rating_total', $total );\n\n        \/\/ Registra voto por IP\n        $voted_ips = get_post_meta( $post_id, '_rating_voted_ips', true );\n        if ( ! is_array( $voted_ips ) ) $voted_ips = [];\n        $voted_ips[] = $_SERVER['REMOTE_ADDR'];\n        update_post_meta( $post_id, '_rating_voted_ips', $voted_ips );\n\n        \/\/ Cookie de 24h\n        setcookie( 'neodoor_rated_' . $post_id, '1', time() + 86400, COOKIEPATH, COOKIE_DOMAIN );\n\n        wp_send_json_success( [ 'html' =&gt; $this-&gt;render_ratings( $post_id ), 'message' =&gt; 'Voto computado!' ] );\n    }\n}\n\nNeodoor_Ratings::get_instance();\n<\/code><\/pre>\n<p><strong>Estilo CSS sugerido (adicione no tema ou no MU-Plugin de CSS):<\/strong><\/p>\n<pre><code>&lt;style&gt;\n.neodoor-ratings-container {\n    display: flex;\n    align-items: center;\n    gap: 12px;\n    margin: 15px 0;\n}\n.neodoor-stars {\n    display: inline-flex;\n    gap: 4px;\n}\n.neodoor-rating-star {\n    font-size: 24px;\n    cursor: pointer;\n    color: #ddd;\n    transition: color 0.2s;\n}\n.neodoor-rating-star.star-filled {\n    color: #ffc107;\n}\n.neodoor-ratings-container.voted .neodoor-rating-star {\n    cursor: default;\n    pointer-events: none;\n}\n.neodoor-rating-text {\n    font-size: 14px;\n    color: #555;\n}\n&lt;\/style&gt;\n<\/code><\/pre>\n<h3>Instru\u00e7\u00f5es de instala\u00e7\u00e3o<\/h3>\n<ol>\n<li>Crie a pasta <code>neodoor-ratings<\/code> dentro de <code>\/wp-content\/mu-plugins\/<\/code>.<\/li>\n<li>Dentro dela, crie o arquivo <code>neodoor-ratings.php<\/code> com o c\u00f3digo acima (e adicione o CSS no seu tema ou no MU-Plugin de estilos).<\/li>\n<li>No conte\u00fado de posts\/p\u00e1ginas onde quiser exibir as estrelas, insira o shortcode <code>[ratings]<\/code>.<\/li>\n<li>Desative e remova o plugin <code>wp-postratings<\/code> e outros similares.<\/li>\n<\/ol>\n<h3>Como testar<\/h3>\n<ul>\n<li>Acesse um post ou p\u00e1gina onde o shortcode foi inserido.<\/li>\n<li>Clique em uma estrela (qualquer uma, de 1 a 5).<\/li>\n<li>Ap\u00f3s o clique, a m\u00e9dia e o total de votos ser\u00e3o atualizados via AJAX, sem recarregar a p\u00e1gina.<\/li>\n<li>Tente votar novamente (mesmo IP\/mesmo cookie) \u2013 o sistema deve bloquear e exibir mensagem.<\/li>\n<li>Se necess\u00e1rio, limpe os cookies ou use um navegador an\u00f4nimo para testar novo voto.<\/li>\n<\/ul>\n<h3>Personaliza\u00e7\u00f5es avan\u00e7adas<\/h3>\n<ul>\n<li><strong>Alterar tempo do cookie:<\/strong> modifique <code>86400<\/code> (segundos) para outro valor (ex: 604800 = 7 dias).<\/li>\n<li><strong>Remover bloqueio por IP:<\/strong> comente as linhas que usam <code>_rating_voted_ips<\/code>.<\/li>\n<li><strong>Estrelas com imagem ou \u00edcone:<\/strong> troque o texto <code>\u2605<\/code> por imagens ou use Font Awesome.<\/li>\n<li><strong>Exibir apenas em certos CPTs:<\/strong> no shortcode, verifique <code>get_post_type()<\/code>.<\/li>\n<\/ul>\n<h3>Plugins substitu\u00eddos<\/h3>\n<ul>\n<li><code>wp-postratings<\/code> \u2013 totalmente substitu\u00eddo.<\/li>\n<li>Qualquer outro plugin de avalia\u00e7\u00e3o por estrelas similar.<\/li>\n<\/ul>\n<h3>Observa\u00e7\u00f5es finais<\/h3>\n<ul>\n<li>O sistema armazena tr\u00eas metadados por post: <code>_rating_average<\/code>, <code>_rating_count<\/code>, <code>_rating_total<\/code> e um array de IPs <code>_rating_voted_ips<\/code>. Em sites muito visitados, esse array pode crescer; recomenda-se limpar ou usar apenas cookies.<\/li>\n<li>O bloqueio por IP + cookie n\u00e3o \u00e9 100% \u00e0 prova de fraude, mas \u00e9 suficiente para a maioria dos sites.<\/li>\n<li>Se voc\u00ea usa cache (WP Super Cache, etc.), o AJAX ainda funciona porque a requisi\u00e7\u00e3o \u00e9 feita diretamente ao admin-ajax.php, n\u00e3o \u00e0 p\u00e1gina cacheada.<\/li>\n<\/ul>\n<h3>Registro no caderno<\/h3>\n<p><strong>Data:<\/strong> <!--?php echo date('d\/m\/Y'); ?--><\/p>\n<p><strong>Funcionalidade:<\/strong> Sistema de vota\u00e7\u00e3o com estrelas (1 a 5) via AJAX.<\/p>\n<p><strong>Plugins substitu\u00eddos:<\/strong> wp-postratings e similares.<\/p>\n<p><strong>Tempo de implementa\u00e7\u00e3o:<\/strong> ~15 minutos.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Aula: Sistema de Vota\u00e7\u00e3o com Estrelas (Rating) Objetivo: Criar um MU-Plugin leve que adiciona um sistema de vota\u00e7\u00e3o de 1 a 5 estrelas em posts, p\u00e1ginas 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\u00e9dia e o total de [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2,4],"tags":[],"class_list":["post-328","post","type-post","status-publish","format-standard","hentry","category-lessons","category-pluginsmu"],"_links":{"self":[{"href":"https:\/\/www.portanova.com.br\/notebook\/wp-json\/wp\/v2\/posts\/328","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.portanova.com.br\/notebook\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.portanova.com.br\/notebook\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.portanova.com.br\/notebook\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.portanova.com.br\/notebook\/wp-json\/wp\/v2\/comments?post=328"}],"version-history":[{"count":0,"href":"https:\/\/www.portanova.com.br\/notebook\/wp-json\/wp\/v2\/posts\/328\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.portanova.com.br\/notebook\/wp-json\/wp\/v2\/media?parent=328"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.portanova.com.br\/notebook\/wp-json\/wp\/v2\/categories?post=328"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.portanova.com.br\/notebook\/wp-json\/wp\/v2\/tags?post=328"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}