

diff --git a/.htaccess b/.htaccess index 56fbae3..eb39e68 100755 --- a/.htaccess +++ b/.htaccess @@ -16,6 +16,9 @@ # Redirect /new to index.php RewriteRule ^new$ index.php?new [NC,L,QSA] + # For votes from post/... + RewriteRule post/vote$ vote.php [NC,L,QSA] + # Show a post's page RewriteRule post/(.+)$ post.php?hash_id=$1 [NC,L,QSA] diff --git a/css/freepost.css b/css/freepost.css index cd55ac8..9bac743 100644 --- a/css/freepost.css +++ b/css/freepost.css @@ -69,40 +69,69 @@ html, body line-height: 1.5em; } - /* Home page */ - .content table.posts + .content .vote { + margin: 0 1.5em 0 0; } - - .content table.posts .bump + + .content .vote > a { - color: #888; + display: inline-block; + margin: 0; + overflow: hidden; + padding: 0; + text-decoration: none; + vertical-align: middle; + } + + .content .vote img { cursor: pointer; - font-size: 1.5em; - font-weight: bold; - padding: 0 0 1em 0; - vertical-align: top; + height: 1em; + margin: 0; + padding: .2em; + float: left; } - .content table.posts .bump > a, - .content table.posts .bump > a:hover - { - color: #AAA; - text-decoration: none; - } + .content .vote .upvoted + { + background-color: #fff; + border: 1px solid #00E313; + border-radius: 999em; + } + + .content .vote .downvoted + { + background-color: #fff; + border: 1px solid #FF0000; + border-radius: 999em; + } - .content table.posts .post + .content .vote .count { + margin: 0 .5em; + } + + /* Home page */ + .content .posts + { + } + + .content .posts .post { - padding: 0 0 1em 1em; + margin: 0 0 2em 0; vertical-align: top; } - .content table.posts .post > .title + .content .posts .post > .title { font-size: 1.5em; } - .content table.posts .post > .info + .content .posts .post > .title > a + { + color: #000; + } + + .content .posts .post > .info { color: #666; } @@ -161,20 +190,28 @@ html, body { margin: 0 0 2em 0; } - - .content > .post > .comments > .comment > .info + + .content > .post > .comments > .comment .pin + { + color: #CD006B; + font-size: .8em; + } + + .content > .post > .comments > .comment .info { font-size: .9em; + padding: 0 0 0 .5em; } - .content > .post > .comments > .comment > .info > .username > a, - .content > .post > .comments > .comment > .info > .username > a:hover + .content > .post > .comments > .comment .info .username > a, + .content > .post > .comments > .comment .info .username > a:hover { + font-weight: bold; padding: 0em 0.5em; } - .content > .post > .comments > .comment > .info > .op > a, - .content > .post > .comments > .comment > .info > .op > a:hover + .content > .post > .comments > .comment > .info .op > a, + .content > .post > .comments > .comment > .info .op > a:hover { background-color: rgb(255, 175, 50); border-radius: 4px; diff --git a/database.php b/database.php index 9a0ee8c..68098c5 100644 --- a/database.php +++ b/database.php @@ -172,21 +172,21 @@ class Database */ function get_post_comments ($post_id) { - $comments = array(); + $comments = array (); - if (is_null($this->database)) + if (is_null ($this->database)) return $comments; - $query = $this->database->prepare( + $query = $this->database->prepare ( 'SELECT C.*, U.`username`' . 'FROM `comment` AS C ' . 'JOIN `user` AS U ON C.`userId` = U.`id`' . 'WHERE C.`postId` = ? ' . 'ORDER BY C.`vote` DESC, C.`created` ASC'); - $query->execute(array($post_id)); + $query->execute (array ($post_id)); - $comments = $query->fetchAll(PDO::FETCH_ASSOC); + $comments = $query->fetchAll (PDO::FETCH_ASSOC); // Group comments by parentId $comments_group = array(); @@ -290,7 +290,7 @@ class Database 'ORDER BY P.`created` DESC ' . 'LIMIT 50'); - $submissions = $query->fetchAll(PDO::FETCH_ASSOC); + $submissions = $query->fetchAll (PDO::FETCH_ASSOC); return $submissions; } @@ -434,6 +434,49 @@ class Database } /** + * Retrieve a list of votes for a range of comments. + * + * @param comments_id list of IDs (eg. "2,4,5"). + * NOTE: Because arrays can't be used with PDO, $comments_id + * is a string that's concatenated to the SQL query. For + * this reason is the responsibility of the caller to + * check that $comments_id is a valid string of integers + * separated by commans (beware of SQL injection). + */ + function get_comments_votes ($comments_id, $user_id) + { + $votes = array(); + + if (is_null ($this->database) || is_null ($comments_id) || is_null ($user_id)) + return $votes; + + // Run a test anyway to make sure $posts_id is a valid string + $comments_id_array = explode (',', $comments_id); + + foreach ($comments_id_array as $comment_id) + if (!is_numeric ($comment_id)) + return $votes; + + // Retrieve the votes + $query = $this->database->prepare ( + 'SELECT * ' . + 'FROM `vote_comment`' . + 'WHERE `commentId` IN(' . $comments_id . ') AND `userId` = ?'); + + $query->execute (array ($user_id)); + + $votes = $query->fetchAll (PDO::FETCH_ASSOC); + + // Create an array of votes with `commentId` as key + $sorted_votes = array(); + + foreach ($votes as $vote) + $sorted_votes[$vote['commentId']] = $vote; + + return $sorted_votes; + } + + /** * Create new user account */ function new_user ($username, $password) @@ -696,13 +739,41 @@ class Database return false; $query = $this->database->prepare( - 'SELECT 1 ' . + 'SELECT * ' . 'FROM `vote_post`' . 'WHERE `postId` = ? and `userId` = ?'); - $query->execute(array($post_id, $user_id)); + $query->execute (array ($post_id, $user_id)); + + $vote = $query->fetch (PDO::FETCH_ASSOC); + + if (is_null ($vote) || empty ($vote)) + return false; - return $query->rowCount() > 0; + return $vote; + } + + /** + * Tell if a user has voted a comment + */ + function voted_comment ($comment_id, $user_id) + { + if (is_null($this->database)) + return false; + + $query = $this->database->prepare( + 'SELECT * ' . + 'FROM `vote_comment`' . + 'WHERE `commentId` = ? and `userId` = ?'); + + $query->execute (array ($comment_id, $user_id)); + + $vote = $query->fetch (PDO::FETCH_ASSOC); + + if (is_null ($vote) || empty ($vote)) + return false; + + return $vote; } /** @@ -718,24 +789,298 @@ class Database $post = self::get_post ($post_hash_id); // Already voted? - $voted = self::voted_post ($post['id'], $user_id); + $vote = self::voted_post ($post['id'], $user_id); - if (!$voted) + if (false == $vote) { // Cast upvote $query = $this->database->prepare( 'INSERT INTO `vote_post` (`vote`, `datetime`, `postId`, `userId`)' . 'VALUES (1, NOW(), ?, ?)'); + + $query->execute (array ($post['id'], $user_id)); + + // Add +1 to post + $query = $this->database->prepare ( + 'UPDATE `post`' . + 'SET `vote` = `vote` + 1 ' . + 'WHERE `id` = ?'); - $query->execute(array($post['id'], $user_id)); + $query->execute (array ($post['id'])); + + } elseif ($vote['vote'] == 1) { + // Already upvoted before. Remove upvote. - // Add +1 to vote + $query = $this->database->prepare( + 'DELETE FROM `vote_post`' . + 'WHERE `postId` = ? AND `userId` = ?'); + + $query->execute (array ($post['id'], $user_id)); + + // Remove upvote from post + $query = $this->database->prepare ( + 'UPDATE `post`' . + 'SET `vote` = `vote` - 1 ' . + 'WHERE `id` = ?'); + + $query->execute (array ($post['id'])); + + } elseif ($vote['vote'] == -1) { + // Already downvoted before. Change to upvote. + + $query = $this->database->prepare( + 'UPDATE `vote_post`' . + 'SET `vote` = 1 ' . + 'WHERE `postId` = ? AND `userId` = ?'); + + $query->execute (array ($post['id'], $user_id)); + + /* Update post vote count + * +2 because of the previous downvote + */ + $query = $this->database->prepare ( + 'UPDATE `post`' . + 'SET `vote` = `vote` + 2 ' . + 'WHERE `id` = ?'); + + $query->execute (array ($post['id'])); + } + + $this->database->commit (); + + } catch(PDOException $ex) { + + $this->database->rollBack(); + + } + } + + /** + * Downvote a post + */ + function downvote_post ($post_hash_id, $user_id) + { + try { + + $this->database->beginTransaction(); + + // Get the post + $post = self::get_post ($post_hash_id); + + // Already voted? + $vote = self::voted_post ($post['id'], $user_id); + + if (false == $vote) + { + // Cast downvote + $query = $this->database->prepare( + 'INSERT INTO `vote_post` (`vote`, `datetime`, `postId`, `userId`)' . + 'VALUES (-1, NOW(), ?, ?)'); + + $query->execute (array ($post['id'], $user_id)); + + // Add -1 to post + $query = $this->database->prepare ( + 'UPDATE `post`' . + 'SET `vote` = `vote` - 1 ' . + 'WHERE `id` = ?'); + + $query->execute (array ($post['id'])); + + } elseif ($vote['vote'] == -1) { + // Already downvoted before. Remove downvote. + + $query = $this->database->prepare( + 'DELETE FROM `vote_post`' . + 'WHERE `postId` = ? AND `userId` = ?'); + + $query->execute (array ($post['id'], $user_id)); + + // Remove downvote from post $query = $this->database->prepare ( 'UPDATE `post`' . 'SET `vote` = `vote` + 1 ' . 'WHERE `id` = ?'); $query->execute (array ($post['id'])); + + } elseif ($vote['vote'] == 1) { + // Already upvoted before. Change to downvote. + + $query = $this->database->prepare( + 'UPDATE `vote_post`' . + 'SET `vote` = -1 ' . + 'WHERE `postId` = ? AND `userId` = ?'); + + $query->execute (array ($post['id'], $user_id)); + + /* Update post vote count + * -2 because of the previous upvote + */ + $query = $this->database->prepare ( + 'UPDATE `post`' . + 'SET `vote` = `vote` - 2 ' . + 'WHERE `id` = ?'); + + $query->execute (array ($post['id'])); + } + + $this->database->commit (); + + } catch(PDOException $ex) { + + $this->database->rollBack(); + + } + } + + /** + * Upvote a comment + */ + function upvote_comment ($comment_hash_id, $user_id) + { + try { + + $this->database->beginTransaction(); + + // Get the comment + $comment = self::get_comment ($comment_hash_id); + + // Already voted? + $vote = self::voted_comment ($comment['id'], $user_id); + + if (false == $vote) + { + // Cast upvote + $query = $this->database->prepare( + 'INSERT INTO `vote_comment` (`vote`, `datetime`, `commentId`, `userId`)' . + 'VALUES (1, NOW(), ?, ?)'); + + $query->execute (array ($comment['id'], $user_id)); + + // Add +1 to comment + $query = $this->database->prepare ( + 'UPDATE `comment`' . + 'SET `vote` = `vote` + 1 ' . + 'WHERE `id` = ?'); + + $query->execute (array ($comment['id'])); + + } elseif ($vote['vote'] == 1) { + // Already upvoted before. Remove upvote. + + $query = $this->database->prepare( + 'DELETE FROM `vote_comment`' . + 'WHERE `commentId` = ? AND `userId` = ?'); + + $query->execute (array ($comment['id'], $user_id)); + + // Remove upvote from comment + $query = $this->database->prepare ( + 'UPDATE `comment`' . + 'SET `vote` = `vote` - 1 ' . + 'WHERE `id` = ?'); + + $query->execute (array ($comment['id'])); + + } elseif ($vote['vote'] == -1) { + // Already downvoted before. Change to upvote. + + $query = $this->database->prepare( + 'UPDATE `vote_comment`' . + 'SET `vote` = 1 ' . + 'WHERE `commentId` = ? AND `userId` = ?'); + + $query->execute (array ($comment['id'], $user_id)); + + /* Update comment vote count + * +2 because of the previous downvote + */ + $query = $this->database->prepare ( + 'UPDATE `comment`' . + 'SET `vote` = `vote` + 2 ' . + 'WHERE `id` = ?'); + + $query->execute (array ($comment['id'])); + } + + $this->database->commit (); + + } catch(PDOException $ex) { + + $this->database->rollBack(); + + } + } + + /** + * Downvote a comment + */ + function downvote_comment ($comment_hash_id, $user_id) + { + try { + + $this->database->beginTransaction(); + + // Get the comment + $comment = self::get_comment ($comment_hash_id); + + // Already voted? + $vote = self::voted_comment ($comment['id'], $user_id); + + if (false == $vote) + { + // Cast downvote + $query = $this->database->prepare( + 'INSERT INTO `vote_comment` (`vote`, `datetime`, `commentId`, `userId`)' . + 'VALUES (-1, NOW(), ?, ?)'); + + $query->execute (array ($comment['id'], $user_id)); + + // Add -1 to comment + $query = $this->database->prepare ( + 'UPDATE `comment`' . + 'SET `vote` = `vote` - 1 ' . + 'WHERE `id` = ?'); + + $query->execute (array ($comment['id'])); + + } elseif ($vote['vote'] == -1) { + // Already downvoted before. Remove downvote. + + $query = $this->database->prepare( + 'DELETE FROM `vote_comment`' . + 'WHERE `commentId` = ? AND `userId` = ?'); + + $query->execute (array ($comment['id'], $user_id)); + + // Remove downvote from comment + $query = $this->database->prepare ( + 'UPDATE `comment`' . + 'SET `vote` = `vote` + 1 ' . + 'WHERE `id` = ?'); + + $query->execute (array ($comment['id'])); + + } elseif ($vote['vote'] == 1) { + // Already upvoted before. Change to downvote. + + $query = $this->database->prepare( + 'UPDATE `vote_comment`' . + 'SET `vote` = -1 ' . + 'WHERE `commentId` = ? AND `userId` = ?'); + + $query->execute (array ($comment['id'], $user_id)); + + /* Update comment vote count + * -2 because of the previous upvote + */ + $query = $this->database->prepare ( + 'UPDATE `comment`' . + 'SET `vote` = `vote` - 2 ' . + 'WHERE `id` = ?'); + + $query->execute (array ($comment['id'])); } $this->database->commit (); diff --git a/images/downvote.png b/images/downvote.png new file mode 100755 index 0000000..3cc8c56 Binary files /dev/null and b/images/downvote.png differ diff --git a/images/upvote.png b/images/upvote.png new file mode 100755 index 0000000..e7e68a4 Binary files /dev/null and b/images/upvote.png differ diff --git a/javascript/freepost.js b/javascript/freepost.js index 5c61a53..5091789 100644 --- a/javascript/freepost.js +++ b/javascript/freepost.js @@ -3,6 +3,58 @@ * to the user after clicks. */ -function hide (dom_element) { - dom_element.style.visibility = 'hidden'; +function vote (action, dom_element) { + var arrow_up = dom_element.children[0]; + var vote_counter = dom_element.children[1]; + var arrow_down = dom_element.children[2]; + + // Voted/Upvoted + var current_status = 0; + + if ("upvoted" == arrow_up.className) + current_status = 1; + + if ("downvoted" == arrow_down.className) + current_status = -1; + + // Current vote + var current_vote = Number (vote_counter.textContent); + + // Remove class from arrows + arrow_up.className = ""; + arrow_down.className = ""; + + // Toggle upvote class for arrow + if ("up" == action) + switch (current_status) + { + case -1: + vote_counter.textContent = current_vote + 2; + arrow_up.className = "upvoted"; + break; + case 0: + vote_counter.textContent = current_vote + 1; + arrow_up.className = "upvoted"; + break; + case 1: + vote_counter.textContent = current_vote - 1; + break; + } + + // Toggle downvote class for arrow + if ("down" == action) + switch (current_status) + { + case -1: + vote_counter.textContent = current_vote + 1; + break; + case 0: + vote_counter.textContent = current_vote - 1; + arrow_down.className = "downvoted"; + break; + case 1: + vote_counter.textContent = current_vote - 2; + arrow_down.className = "downvoted"; + break; + } } \ No newline at end of file diff --git a/post.php b/post.php index 0af8ae7..5be3509 100644 --- a/post.php +++ b/post.php @@ -56,11 +56,20 @@ if (empty ($post)) } // Retrieve if user has voted this post -$votes = $db->get_posts_votes ($post['id'], Session::get_userid ()); +$votes_post = $db->get_posts_votes ($post['id'], Session::get_userid ()); // Retrieve comments for this post $comments = $db->get_post_comments ($post['id']); +// Retrieve a list of user votes for the comments +$IDs = array(); + +foreach ($comments as $parent) + foreach ($parent as $child) + $IDs[] = $child['id']; + +$votes_comment = $db->get_comments_votes (implode (',', $IDs), Session::get_userid ()); + // Render template echo $twig->render ( 'post.twig', @@ -68,7 +77,9 @@ echo $twig->render ( 'title' => $post['title'], 'post' => $post, 'comments' => $comments, - 'votes' => $votes)); + 'votes' => array ( + 'post' => $votes_post, + 'comment' => $votes_comment))); diff --git a/template/comment.twig b/template/comment.twig index 6b40fbc..8cacf9f 100644 --- a/template/comment.twig +++ b/template/comment.twig @@ -1,38 +1,56 @@ {% if comments[parent_id] %} {% for comment in comments[parent_id] %} -
▣ | - {# DateTime #} - {{ comment.created|ago }} - - — - - {# Reply #} - Reply - - {# Edit #} - {% if user and comment.userId == user.id %} - Edit - {% endif %} - - - {{ comment.text|markdown|raw }} - ++ {# Username #} + + {{ comment.username }} + + + {% + include 'vote.twig' with { + target: 'comment', + hash_id: comment.hashId, + vote: votes[comment.id] is defined ? votes[comment.id].vote : null, + vote_count: comment.vote + } only + %} + + {# DateTime #} + {{ comment.created|ago }} + + — + + {# Reply #} + Reply + + {# Edit #} + {% if user and comment.userId == user.id %} + Edit + {% endif %} + | +
+ | + {{ comment.text|markdown|raw }} + | +
- {% if not votes[post.id] %}
- ▲
+
+
+
+ {% if post.link|length > 0 %}
+
+ {{ post.title }}
+
+ {% else %}
+
+ {{ post.title }}
+
{% endif %}
- |
+
-
-
- {% if post.link|length > 0 %}
-
- {{ post.title }}
-
- {% else %}
-
- {{ post.title }}
-
- {% endif %}
-
+
+ {%
+ include 'vote.twig' with {
+ target: 'post',
+ hash_id: post.hashId,
+ vote: votes[post.id] is defined ? votes[post.id].vote : null,
+ vote_count: post.vote
+ } only
+ %}
+
+ {{ post.created|ago }}
+ by {{ post.username }} •
-
- {{ post.vote }} votes
- {{ post.created|ago }}
- by {{ post.username }} •
-
- {{ post.commentsCount ? post.commentsCount ~ ' comments' : 'discuss' }}
-
- |
-
- {% if not votes[post.id] %} - ▲ - {% endif %} - | - -
-
- {% if post.link|length > 0 %}
-
- {{ post.title }}
-
- {% else %}
- {{ post.title }}
- {% endif %}
-
+
+ {% if post.link|length > 0 %}
+
+ {{ post.title }}
+
+ {% else %}
+ {{ post.title }}
+ {% endif %}
+
-
- by {{ post.username }} {{ post.created|ago }}
- — {{ post.vote }} votes, {{ post.commentsCount }} comments
-
- {% if user and post.userId == user.id %}
- — Edit
- {% endif %}
-
- |
-