DEV Community

arslonga
arslonga

Posted on

3 3

Like/Dislike system with Mojolicious

This post describes the actual Like/Dislike mechanism in the program based on Mojolicious. Web pages style is created using the Bootstrap framework. The full version of the Mojolicious like-dislike demo can be downloaded at: https://github.com/arslonga/like-dislike

So, in the start package Vote.pm we create routers for the post with a certain ID in the section named 'first-section' (for example), and also for like and dislike:

sub startup {
my $self = shift;

my $r = $self->routes;
...
$r->any('/first-section/:id' => [ id => qr/[0-9]+/ ] )->to('post#article');
...
$r->any('/likeartcl')->to('voting#like');
$r->any('/dislikeartcl')->to('voting#dislike');
...
}
Enter fullscreen mode Exit fullscreen mode

A snippet of code in the Post.pm package that describes the 'article' method:

package Vote::Controller::Post;
use Mojo::Base 'Mojolicious::Controller';
use Session;
use SessCheck;
...
#---------------------------------
sub article {
#---------------------------------
my $self = shift;
my $id = $self->stash('id');
my($nickname, $status, $client_check, $user_id, $title_alias);


eval{
$nickname = $self->session('client')->[0];
$user_id = $self->session('client')->[2]; 
};
$client_check = SessCheck->client( $self, $nickname );
if( !$client_check ){
    $status = ' disabled';
}
$title_alias = (split(/\//, $self->req->url))[1];
my $data = $self->db->select( 'posts', ['*'], {id => $id} )->hash;

$self->render( 
dat             => $data,
section_ident   => $title_alias,
id              => $id,
like_id         => 'like_'.$title_alias.'_'.$id,
unlike_id       => 'unlike_'.$title_alias.'_'.$id,
client_id       => $user_id,
unlike_btn_name => 'unlike'.$title_alias.'_'.$id,
like_btn_name   => 'like'.$title_alias.'_'.$id,
status          => $status,
liked_cnt       => $data->{liked} || 0,
unliked_cnt     => $data->{unliked} || 0
);
}#---------------
...
1;
Enter fullscreen mode Exit fullscreen mode

Template article.html.ep in /templates/post

% layout 'vote';

<hr>
<a class="btn btn-info" href="/first-section" role="button">
<span class="glyphicon glyphicon-arrow-left"></span>
</a>
<hr>
<div>
    <h2><%= $dat->{title} %></h2>
    <%= $dat->{body} %>
</div>
<hr>
<div class="ld-box text-right">

%# Rendering a template that describes a block of code
%# that contains two buttons:
%# 'like' and 'dislike'

<%= include 'voting/vote' %>
</div>

<script>
//---------- Like/Dislike block ---
async function LikeArtcl(titleAlias, articleId, vote_span, user_id) {
let likedislikeBox = document.querySelector('.ld-box');

  let response = await fetch("/likeartcl?title_alias=" + 
  titleAlias + 
  '&article_id=' + 
  articleId + 
  '&vote_span=' + 
  vote_span + 
  '&user_id=' + 
  user_id);

  if (response.ok) {
    let respRendr = await response.text(); 
    likedislikeBox.innerHTML = respRendr;
  }else {
    alert("Error HTTP: " + response.status);
  }
}

async function UnlikeArtcl(titleAlias, articleId, vote_span, user_id) {
let likedislikeBox = document.querySelector('.ld-box');

  let response = await fetch("/dislikeartcl?title_alias=" + 
  titleAlias + 
  '&article_id=' + 
  articleId + 
  '&vote_span=' + 
  vote_span + 
  '&user_id=' + 
  user_id);

  if (response.ok) {
    let respRendr = await response.text(); 
    likedislikeBox.innerHTML = respRendr;
  }else {
    alert("Error HTTP: " + response.status);
  }
}
//---------- Like/Dislike block END ---
</script>
Enter fullscreen mode Exit fullscreen mode

Package Session.pm:

package Session;
use Mojo::Base -base;

#---------------------------------
sub user {
#---------------------------------
my($self, $c, $login, $password, $id) = @_;

$c->session( client => [$login, $password, $id], expiration => 120);
return 1;
}#---------------

# 'voting' method of Session called in 'Voting.pm' package (see code fragment below)
#---------------------------------
sub voting {
#---------------------------------
my($self, $c, $user_vote_id, $title_alias_and_id) = @_;
$c->signed_cookie( $user_vote_id => $title_alias_and_id, {expires => time + 120});
return 1;
}#---------------

#---------------------------------
sub client_expire {
#---------------------------------
my($self, $c) = @_;
delete $c->session->{'client'};
return 1;
}#---------------
1;
Enter fullscreen mode Exit fullscreen mode

Package Vote/Voting.pm
Here we render template 'voting/vote.html.ep' that describes code block with 'like' and 'dislike' buttons and 'like' and 'dislike' counts

package Vote::Voting;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::Util qw(trim encode decode);
use Mojo::Cookie;
use Session;

#---------------------------------
sub like {
#---------------------------------
my $self = shift;
my($nickname, $status);
eval{
$nickname = $self->session('client')->[0];  
};
my $client_check = SessCheck->client( $self, $nickname );

my $title_alias = $self->param('title_alias');
my $article_id  = $self->param('article_id');
my $user_id     = $self->param('user_id');

$status = !$client_check ? ' disabled' : '';

my $like_count = $self->db->select( 'posts', 
                                  ['liked'], 
                                  {id => $article_id} )
                                  ->hash->{liked};
my $unlike_count = $self->db->select( 'posts', 
                                    ['unliked'], 
                                    {id => $article_id} )
                                    ->hash->{unliked};
my $like_cookie_name = 'like_user'.$user_id.'_'.$title_alias.'_'.$article_id;
my $unlike_cookie_name = 'unlike_user'.$user_id.'_'.$title_alias.'_'.$article_id;

if( !$self->signed_cookie( $like_cookie_name ) && $client_check ){
    ++$like_count;
    $self->db->update( 'posts', 
                     {'liked' => $like_count}, 
                     {'id' => $article_id} );
}

if( $self->signed_cookie( $unlike_cookie_name ) ){
    --$unlike_count;
    $self->db->update( 'posts', 
                     {'unliked' => $unlike_count}, 
                     {'id' => $article_id} );
    $self->signed_cookie( $unlike_cookie_name => '', {expires => 1});
}

# Store session for 'like' action where key is 
#'like_user'.$user_id.'_'.$title_alias.'_'.$article_id
# and value is $title_alias.'_'.$article_id
Session->voting( $self, 
                 'like_user'.$user_id.'_'.$title_alias.'_'.$article_id, 
                 $title_alias.'_'.$article_id 
               );

$self->render(
template        => 'voting/vote',
section_ident   => $title_alias,
id              => $article_id,
like_id         => 'like_'.$title_alias.'_'.$article_id,
unlike_id       => 'unlike_'.$title_alias.'_'.$article_id,
client_id       => $user_id,
unlike_btn_name => 'unlike'.$title_alias.'_'.$article_id,
like_btn_name   => 'like'.$title_alias.'_'.$article_id,
status          => $status,
liked_cnt       => $like_count,
unliked_cnt     => $unlike_count
);
}#---------------

#---------------------------------
sub dislike {
#---------------------------------
my $self = shift;
my($nickname, $status);
eval{
$nickname = $self->session('client')->[0];  
};
my $client_check = SessCheck->client( $self, $nickname );

my $title_alias = $self->param('title_alias');
my $article_id  = $self->param('article_id');
my $user_id     = $self->param('user_id');

$status = !$client_check ? ' disabled' : '';

my $unlike_count = $self->db->select( 'posts', 
                                    ['unliked'], 
                                    {id => $article_id} )
                                    ->hash->{unliked};
my $like_count = $self->db->select( 'posts', 
                                  ['liked'], 
                                  {id => $article_id} )
                                  ->hash->{liked};
my $like_cookie_name = 'like_user'.$user_id.'_'.$title_alias.'_'.$article_id;
my $unlike_cookie_name = 'unlike_user'.$user_id.'_'.$title_alias.'_'.$article_id;

if( !$self->signed_cookie( $unlike_cookie_name ) && $client_check ){
    ++$unlike_count;
    $self->db->update( 'posts', 
                     {'unliked' => $unlike_count}, 
                     {'id' => $article_id} );
}

if( $self->signed_cookie( $like_cookie_name ) ){
    --$like_count;
    $self->db->update( 'posts', 
                     {'liked' => $like_count}, 
                     {'id' => $article_id} );
    $self->signed_cookie( $like_cookie_name => '', {expires => 1});
}

# Store session for 'dislike' action where key is 
#'unlike_user'.$user_id.'_'.$title_alias.'_'.$article_id
# and value is $title_alias.'_'.$article_id
Session->voting( $self, 
                 'unlike_user'.$user_id.'_'.$title_alias.'_'.$article_id, 
                 $title_alias.'_'.$article_id );

$self->render(
template        => 'voting/vote',
section_ident   => $title_alias,
id              => $article_id,
like_id         => 'like_'.$title_alias.'_'.$article_id,
unlike_id       => 'unlike_'.$title_alias.'_'.$article_id,
client_id       => $user_id,
unlike_btn_name => 'unlike'.$title_alias.'_'.$article_id,
like_btn_name   => 'like'.$title_alias.'_'.$article_id,
status          => $status,
unliked_cnt     => $unlike_count,
liked_cnt       => $like_count
);
}#---------------
1;

Enter fullscreen mode Exit fullscreen mode

Template voting/vote.html.ep
Use it for both rendering 'like' and 'dislike' actions

%# voting/vote.html.ep

<button type="button" name="<%= $like_btn_name %>" 
onclick="Voting('<%= $section_ident %>', 
                   '<%= $id %>', 
                   '<%= $like_id %>', 
                   '<%= $client_id %>'); 
                   this.disabled='disabled';" 
                   id="chevron_stl"<%= $status %>>
<span id="<%= $section_ident.'_'.$client_id.'-up' %>" 
class="glyphicon glyphicon-chevron-up" aria-hidden="true"></span>
</button>
<span id="<%= $like_id %>" class="like_unlike"><%= $liked_cnt %></span>

<button type="button" name="<%= $unlike_btn_name %>" 
onclick="Voting('<%= $section_ident %>', 
                     '<%= $id %>', 
                     '<%= $unlike_id %>', 
                     '<%= $client_id %>'); 
                     this.disabled='disabled';" 
                     id="chevron_stl"<%= $status %>>
<span id="<%= $section_ident.'_'.$client_id.'-down' %>" 
class="glyphicon glyphicon-chevron-down" aria-hidden="true"></span>
</button>
<span id="<%= $unlike_id %>" class="like_unlike"><%= $unliked_cnt %></span>
Enter fullscreen mode Exit fullscreen mode

Conclusion

Use the combination of article ID, user ID, and kind of action (like or dislike) to create a unique voting ID. This voting ID can be a key of signed cookie.

A simplified like/dislike system implemented in my project MornCat CMS at https://github.com/arslonga/my_blog

SurveyJS custom survey software

JavaScript UI Libraries for Surveys and Forms

SurveyJS lets you build a JSON-based form management system that integrates with any backend, giving you full control over your data and no user limits. Includes support for custom question types, skip logic, integrated CCS editor, PDF export, real-time analytics & more.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay