How to Add Voting to bbPress Topics and Replies

bbPress Voting

If you have a WordPress site with a bbPress Forum and you’re looking to add user voting functionality, then you’ve come to the right place.

As an experienced WordPress developer and the developer of the free bbPress Voting plugin, I have extensive experience with this that I’m happy to share with you.

In this article, I will be sharing with you lots of code snippets that you can use to implement voting on your bbPress forum. However, you might find it a lot more simple and it would include a lot more features if you just install my free plugin.

But, if you’re the type who likes to code it yourself, or you just want to learn how to do it to get better at WordPress development, then read on.

Adding the Voting Up & Down Buttons

In this code snippet, we are hooking into 3 different action hooks that are exposed by bbPress and we are running the same function for all them which will handle adding the voting buttons.

In that function, first it uses both the current post_type and the current_action() (to know which hook is being used) to get the post data.

After we get the post data for that topic or reply, we get some post meta out of it which we will be using to store the ups, downs, score, and voting log.

Voting tracking uses the logged in user ID or the IP address of a not-logged-in visitor.

With all of that, we build the HTML that will be echoed at the end of the function.

add_action('bbp_theme_before_topic_title', 'wpftw_bbp_voting_buttons');
add_action('bbp_theme_before_topic_content', 'wpftw_bbp_voting_buttons');
add_action('bbp_theme_before_reply_content', 'wpftw_bbp_voting_buttons');

function wpftw_bbp_voting_buttons() {
    $topic_post_type = bbp_get_topic_post_type();
    $reply_post_type = bbp_get_reply_post_type();
    if(current_action() === 'bbp_theme_before_topic_title' || current_action() == 'bbp_theme_before_topic_content') {
        // Topic hook will always be the topic
        $this_post_type = $topic_post_type;
    }
    if(current_action() === 'bbp_theme_before_reply_content') {
        // Reply hook could be topic (OP) or a reply
        $this_post_type = bbp_voting_get_current_post_type();
    }
    // Get the post
    if($this_post_type == $topic_post_type) $post = bbpress()->topic_query->post;
    if($this_post_type == $reply_post_type) $post = bbpress()->reply_query->post;
    // Do we have a post?
    if(!empty($post)) {
        $post_id = $post->ID;
        $score = (int) get_post_meta($post_id, 'bbp_voting_score', true);
        $ups = (int) get_post_meta($post_id, 'bbp_voting_ups', true);
        $downs = (int) get_post_meta($post_id, 'bbp_voting_downs', true);
        // Check for, and correct, discrepancies
        $calc_score = $ups + $downs;
        if($score > $calc_score) {
            $diff = $score - $calc_score;
            $ups += $diff;
            update_post_meta($post_id, 'bbp_voting_ups', $ups);
        }
        // Get user's vote by ID or IP
        $voting_log = get_post_meta($post_id, 'bbp_voting_log', true);
        $voting_log = is_array($voting_log) ? $voting_log : array(); // Set up new array
        $client_ip = $_SERVER['REMOTE_ADDR'];
        $identifier = is_user_logged_in() ? get_current_user_id() : $client_ip;
        $existing_vote = array_key_exists($identifier, $voting_log) ? $voting_log[$identifier] : 0;
        // Start HTML
        $html = '';
        $html .= '<div class="wpftw-bbp-voting wpftw-bbp-voting-post-'.$post_id. ($existing_vote == 1 ? ' voted-up' : ($existing_vote == -1 ? ' voted-down' : '')) .'">';
        // Up vote
        $html .= '<a class="vote up" data-votes="'.($ups ? '+'.$ups : '').'" onclick="wpftw_bbpress_post_vote_link_clicked(' . $post_id . ', 1); return false;">Up</a>';
        // Display current vote count for post
        $html .= '<div class="score">'. $score. '</div>';
        // Down vote
        $html .= '<a class="vote down" data-votes="'.($downs ? $downs : '').'" onclick="wpftw_bbpress_post_vote_link_clicked(' . $post_id . ', -1); return false;">Down</a>';
        $html .= '</div>';
        echo $html;
    }
}

Style the Voting Buttons with SCSS

With this SCSS (Sass) which you can compile down to plain CSS will handle the styling of the voting buttons. It makes the up and down buttons be arrows with green and red hover colors, and it has the score in a large font in the middle. It looks just like what you would find on Stack Overflow forums.

$green: green;
$red: #a11717;
.wpftw-bbp-voting {
    margin: 8px 0;
    padding-right: 30px;
    min-width: 65px;
    .score {
        font-size: 25px;
        font-weight: bold;
        text-align: center;
        color: #858c93;
        padding: 2px 0 3px 0;
    }
    a.vote {
        display: block !important;
        position: relative;
        overflow: visible;
        text-indent: -9999em;
        width: 0; 
        height: 0; 
        margin: 3px auto !important;
        border: inherit !important;
        border-left: 15px solid transparent !important;
        border-right: 15px solid transparent !important;
        cursor: pointer;
        &.up {
            border-bottom: 15px solid #858c93 !important;
            &:hover {
                border-bottom: 15px solid #444 !important;
            }
        }
        &.down {
            border-top: 15px solid #858c93 !important;
            &:hover {
                border-top: 15px solid #444 !important;
            }
        }
        &:after {
            content: attr(data-votes);
            display: block;
            position: absolute;
            top: -10px;
            text-align: left;
            margin-left: 17px;
            text-indent: 0;
            color: #aaa;
            opacity: 0;
            transition: opacity 500ms ease;
        }
    }
    &.voted-up a.vote.up {
        border-bottom-color: $green !important;
    }
    &.voted-down a.vote.down {
        border-top-color: $red !important;
    }
}
ul.bbp-topics {
    .wpftw-bbp-voting {
        float: left;
    }
}

AJAX to Handle Vote Clicks

Now that we have buttons and styled them, we need to make the buttons do something. We need to add an AJAX handler using the standard WordPress way of doing it.

Add a function to hook into wp_enqueue_scripts and in there, you will enqueue a javascript file and also use wp_localize_script to make it accessible via AJAX through WordPress.

add_action('wp_enqueue_scripts', 'wpftw_bbp_voting_scripts');

function wpftw_bbp_voting_scripts() {
    wp_enqueue_script( 'wpftw-bbp-voting-js', plugin_dir_url(__FILE__) . '/wpftw-bbp-voting.js', array('jquery'), filemtime(plugin_dir_path(__FILE__). 'wpftw-bbp-voting.js') );
    wp_localize_script( 'wpftw-bbp-voting-js', 'wpftw_bbp_voting_ajax_object', array( 'ajax_url' => admin_url( 'admin-ajax.php' ) ) );
}

The content of that javascript file should look something like this below. It handles that javascript that executes when you click a vote button, sends the data via AJAX and handles the response from the server.

function wpftw_bbpress_post_vote_link_clicked(post_id, direction) {
    var post_clicked = jQuery('.wpftw-bbp-voting.wpftw-bbp-voting-post-'+post_id);
    // Loading CSS
    post_clicked.css('opacity', 0.5).css('pointer-events', 'none');
    // Ajax data
    var data = {
        'action': 'wpftw_bbpress_post_vote_link_clicked',
        'post_id': post_id,
        'direction': direction
    };
    jQuery.post(wpftw_bbp_voting_ajax_object.ajax_url, data, function(response) {
        if(response.hasOwnProperty('error')) {
            // Error response
            console.log('Voting error:', response.error);
        } else if(!response.hasOwnProperty('score')) {
            // Catch invalid AJAX response
            console.log('SOMETHING WENT WRONG', response);
        } else {
            // Proper response that has score, direction, ups, and downs
            var score = parseInt(response.score);
            direction = parseInt(response.direction);
            var up = parseInt(response.ups);
            var down = parseInt(response.downs);
            console.log('Voted ' + direction, 'post #' + post_id, 'score: ' + score, 'ups: ' + up, 'downs: ' + down);
            jQuery('.wpftw-bbp-voting.wpftw-bbp-voting-post-'+post_id).each(function() {
                // Get elements
                var wrapper = jQuery(this);
                var score_el = jQuery(this).find('.score');
                var up_el = jQuery(this).find('.up');
                var down_el = jQuery(this).find('.down');
                // Set elements' html
                score_el.html(score);
                up_el.attr('data-votes', '+' + up);
                down_el.attr('data-votes', down);
                // Change arrow colors
                if(direction > 0) {
                    // Up vote
                    up_el.css('border-bottom-color', '#1e851e');
                    wrapper.removeClass('voted-down').addClass('voted-up');
                } else if (direction < 0) {
                    // Down vote
                    down_el.css('border-top-color', '#992121');
                    wrapper.removeClass('voted-up').addClass('voted-down');
                } else if (direction == 0) {
                    // Remove vote
                    up_el.css('border-bottom-color', 'inherit');
                    down_el.css('border-top-color', 'inherit');
                    wrapper.removeClass('voted-down').removeClass('voted-up');
                }
                // Restore the CSS
                wrapper.css('opacity', 1).css('pointer-events', 'auto');
            });
        }
    });
}

Server-side API to Receive the Vote Click AJAX Call

Now, we need to write the server-side PHP code that is executed by that AJAX call. This takes care of actually updating the post meta values in the database and responding to the AJAX call.

// Process a vote

add_action('wp_ajax_bbpress_post_vote_link_clicked','wpftw_bbpress_post_add_vote');
add_action('wp_ajax_nopriv_bbpress_post_vote_link_clicked','wpftw_bbpress_post_add_vote');

function wpftw_bbpress_post_add_vote(){
    header('Content-type: application/json');
    $post_id = (int) $_POST['post_id'];
    // Direction
    $direction = (int) $_POST['direction'];
    $direction = in_array($direction, [1, -1]) ? $direction : 0; // Enforce 1 or -1
    $voting_log = get_post_meta($post_id, 'bbp_voting_log', true);
    $voting_log = is_array($voting_log) ? $voting_log : array(); // Set up new array
    $client_ip = $_SERVER['REMOTE_ADDR'];
    $identifier = is_user_logged_in() ? get_current_user_id() : $client_ip;
    $remove_vote = false;
    $reverse_vote = false;
    // Catch user voted already (by user ID or IP)
    if(array_key_exists($identifier, $voting_log)) {
        // Identifier found
        if($voting_log[$identifier] == $direction) {
            // Voting again in the same direction
            $remove_vote = true;
        } elseif($voting_log[$identifier] == $direction * -1 ) {
            // Changing the vote in different direction
            $reverse_vote = true;
        } else {
            // Changing vote from 0
        }
    } 
    // All good, add the user's vote
    // But first get all the data
    $score = (int) get_post_meta($post_id, 'bbp_voting_score', true);
    $ups = $ups_og = (int) get_post_meta($post_id, 'bbp_voting_ups', true);
    $downs = $downs_og = (int) get_post_meta($post_id, 'bbp_voting_downs', true);
    if($direction > 0) {
        // Up vote
        if($remove_vote) {
            $ups = $ups - 1;
            $score = $score - 1;
        } else {
            $ups = $ups + 1;
            $score = $score + 1;
        }
        if($reverse_vote) {
            $downs = $downs + 1;
            $score = $score + 1;
        }
    } elseif($direction < 0) {
        // Down vote
        if($remove_vote) {
            $downs = $downs + 1;
            $score = $score + 1;
        } else {
            $downs = $downs - 1;
            $score = $score - 1;
        }
        if($reverse_vote) {
            $ups = $ups - 1;
            $score = $score - 1;
        }
    }
    // Update the score
    update_post_meta($post_id, 'bbp_voting_score', $score);
    // Update the ups and downs if needed
    if($ups !== $ups_og) update_post_meta($post_id, 'bbp_voting_ups', $ups);
    if($downs !== $downs_og) update_post_meta($post_id, 'bbp_voting_downs', $downs);
    // Log the user's ID or IP
    $real_direction = $remove_vote ? 0 : $direction;
    $voting_log[$identifier] = $real_direction;
    update_post_meta($post_id, 'bbp_voting_log', $voting_log);
    echo json_encode(array(
        'score' => $score,
        'direction' => $real_direction,
        'ups' => $ups,
        'downs' => $downs
    ));
    exit;
}

There’s a Better Way

Again, I share all these details and code snippets with you for your learning, but if you want all this and much more features packed into it, just download my free bbPress Voting plugin from the WordPress Plugin Directory.

I hope you enjoy it!

If you have any questions about any of this code, please comment below. If you need support for the plugin, please use the Support tab on the plugin page at wordpress.org. Thanks!

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments