How to create Pinterest-like script – step 5

How to create Pinterest-like script – step 5

How to create Pinterest-like script - step 5

How to create Pinterest-like script – step 5

Today – the first article in 2013. We are about to finish our Pinterest like script. In our fifth lesson I prepared next things: like and repin functionality and search. As you know, ‘like’ is a kind of a rating system. In our script – any logged member can rate any certain photo (to like it) once a hour (it is a protection system against cheating). If you like a photo and want to add it to your profile – you can click ‘repin’ button. This will add a copy of this photo for you (actually – only a new record to database). As for the search – everything is easy: we prepared this search bar long ago, but it has not worked before. I added this functionality today. We are going to publish updated sources of our script in our lesson. If you are ready – let’s start.

It is the very time to try our updated demonstration and download the source package here:

Live Demo

Step 1. SQL

In order to implement like counter and repin functionality we have to expand our `pd_photos` table.

CREATE TABLE IF NOT EXISTS `pd_photos` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `title` varchar(255) default '',
  `filename` varchar(255) default '',
  `owner` int(11) NOT NULL,
  `when` int(11) NOT NULL default '0',
  `comments_count` int(11) NOT NULL default '0',
  `repin_id` int(11) NOT NULL default '0',
  `repin_count` int(11) NOT NULL default '0',
  `like_count` int(11) NOT NULL default '0',
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

If you want only update your existed table, please execute only this small SQL:

ALTER TABLE `pd_photos`
 ADD `repin_id` int(11) NOT NULL default '0',
 ADD `repin_count` int(11) NOT NULL default '0',
 ADD `like_count` int(11) NOT NULL default '0';

Finally, I prepared one new SQL table to keep likes:

CREATE TABLE IF NOT EXISTS `pd_items_likes` (
  `l_id` int(11) NOT NULL AUTO_INCREMENT ,
  `l_item_id` int(12) NOT NULL default '0',
  `l_pid` int(12) NOT NULL default '0',
  `l_when` int(11) NOT NULL default '0',
  PRIMARY KEY (`l_id`),
  KEY `l_item_id` (`l_item_id`)
) ENGINE=MYISAM DEFAULT CHARSET=utf8;

Step 2. PHP

I decided to display search result at our ‘index.php’ page, we need to make few minor changes here. Here is updated version:

index.php

require_once('classes/CMySQL.php');
require_once('classes/CMembers.php');
require_once('classes/CPhotos.php');

// get login data
list ($sLoginMenu, $sExtra) = $GLOBALS['CMembers']->getLoginData();

// get search keyword (if provided)
$sSearchParam = strip_tags($_GET['q']);

// get all photos
$sPhotos = $GLOBALS['CPhotos']->getAllPhotos(0, $sSearchParam);

if ($sSearchParam) {
    $sExtra .= '<h2 class="pname">Search results for <strong>'.$sSearchParam.'</strong></h2>';
}

// draw common page
$aKeys = array(
    '{menu_elements}' => $sLoginMenu,
    '{extra_data}' => $sExtra,
    '{images_set}' => $sPhotos
);
echo strtr(file_get_contents('templates/index.html'), $aKeys);

Now, as you remember, we use ‘service.php’ file to perform various service methods. Please review our updated version (where I added possibilities to work with ‘like’ and ‘repin’ buttons:

service.php

require_once('classes/CMySQL.php');
require_once('classes/CMembers.php');
require_once('classes/CPhotos.php');
require_once('classes/CComments.php');

if (! isset($_SESSION['member_id']) && $_POST['Join'] == 'Join') {
    $GLOBALS['CMembers']->registerProfile();
}

$i = (int)$_GET['id'];

if ($_GET && $_GET['get'] == 'comments') {
    header('Content-Type: text/html; charset=utf-8');
    echo $GLOBALS['Comments']->getComments($i);
    exit;
}
if ($_POST) {
    header('Content-Type: text/html; charset=utf-8');
    if ($_SESSION['member_id'] && $_SESSION['member_status'] == 'active' && $_SESSION['member_role']) {
        switch($_POST['add']) {
            case 'comment':
                echo $GLOBALS['Comments']->acceptComment(); exit;
                break;
            case 'like':
                echo $GLOBALS['CPhotos']->acceptLike(); exit;
                break;
            case 'repin':
                echo $GLOBALS['CPhotos']->acceptRepin(); exit;
                break;
        }
    }
    echo '<h3>Please login first</h3>';
    exit;
}

if (! $i) { // if something is wrong - relocate to error page
    header('Location: error.php');
    exit;
}

$aPhotoInfo = $GLOBALS['CPhotos']->getPhotoInfo($i);
$aOwnerInfo = $GLOBALS['CMembers']->getProfileInfo($aPhotoInfo['owner']);

$sOwnerName = ($aOwnerInfo['first_name']) ? $aOwnerInfo['first_name'] : $aOwnerInfo['email'];
$sPhotoTitle = $aPhotoInfo['title'];
$sPhotoDate = ($aPhotoInfo['repin_id'] == 0) ? 'Uploaded on ' : 'Repinned on ';
$sPhotoDate .= $GLOBALS['CPhotos']->formatTime($aPhotoInfo['when']);

$sFolder = 'photos/';
$sFullImgPath = $sFolder . 'f_' . $aPhotoInfo['filename'];
$aSize = getimagesize($sFullImgPath); // get image info
$iWidth = $aSize[0];
$iHeight = $aSize[1];

// repin possibility to logged members
$iLoggId = (int)$_SESSION['member_id'];
$sActions = ($iLoggId && $aPhotoInfo['owner'] != $iLoggId) ? '<a href="#" class="button repinbutton" onclick="return repinPhoto(this);">Repin</a>' : '';

?>
<div class="pin bigpin" bpin_id="<?= $i ?>">
    <div class="owner">
        <a href="#" class="button follow_button">Follow</a>
        <a class="owner_img" href="profile.php?id=<?= $aOwnerInfo['id'] ?>">
            <img alt="<?= $sOwnerName ?>" src="images/avatar.jpg" />
        </a>
        <p class="owner_name"><a href="profile.php?id=<?= $aOwnerInfo['id'] ?>"><?= $sOwnerName ?></a></p>
        <p class="owner_when"><?= $sPhotoDate ?></p>
    </div>
    <div class="holder">
        <div class="actions">
            <?= $sActions ?>
        </div>
        <a class="image" href="#" title="<?= $sPhotoTitle ?>">
            <img alt="<?= $sPhotoTitle ?>" src="<?= $sFullImgPath ?>" style="width:<?= $iWidth ?>px;height:<?= $iHeight ?>px;" />
        </a>
    </div>

    <p class="desc"><?= $sPhotoTitle ?></p>

    <div class="comments"></div>

    <script>
    function submitCommentAjx() {
        $.ajax({ 
          type: 'POST',
          url: 'service.php',
          data: 'add=comment&id=' + <?= $i ?> + '&comment=' + $('#pcomment').val(),
          cache: false, 
          success: function(html){
            if (html) {
              $('.comments').html(html);
              $(this).colorbox.resize();
            }
          } 
        });
    }
    function repinPhoto(obj) {
        var iPinId = $(obj).parent().parent().parent().attr('bpin_id');
        $.ajax({ 
          url: 'service.php',
          type: 'POST',
          data: 'add=repin&id=' + iPinId,
          cache: false, 
          success: function(res){
            window.location.href = 'profile.php?id=' + res;
          } 
        });
        return false;
    }
    </script>
    <form class="comment" method="post" action="#">
        <textarea placeholder="Add a comment..." maxlength="255" id="pcomment"></textarea>
        <button type="button" class="button" onclick="return submitCommentAjx()">Comment</button>
    </form>
</div>

Next updated file is the main Photos class:

classes/CPhotos.php

/*
* Photos class
*/
class CPhotos {

    // constructor
    function CPhotos() {
    }

    // get all photos
    function getAllPhotos($iPid = 0, $sKeyPar = '') {

        // prepare WHERE filter
        $aWhere = array();
        if ($iPid) {
            $aWhere[] = "`owner` = '{$iPid}'";
        }
        if ($sKeyPar != '') {
            $sKeyword = $GLOBALS['MySQL']->escape($sKeyPar);
            $aWhere[] = "`title` LIKE '%{$sKeyword}%'";
        }
        $sFilter = (count($aWhere)) ? 'WHERE ' . implode(' AND ', $aWhere) : '';

        $sSQL = "
            SELECT * 
            FROM `pd_photos`
            {$sFilter}
            ORDER BY `when` DESC
        ";
        $aPhotos = $GLOBALS['MySQL']->getAll($sSQL);

        $sPhotos = '';
        $sFolder = 'photos/';
        foreach ($aPhotos as $i => $aPhoto) {

            $iPhotoId = (int)$aPhoto['id'];
            $sFile = $aPhoto['filename'];
            $sTitle = $aPhoto['title'];
            $iCmts = (int)$aPhoto['comments_count'];

            $iLoggId = (int)$_SESSION['member_id'];
            $iOwner = (int)$aPhoto['owner'];
            $iRepins = (int)$aPhoto['repin_count'];
            $iLikes = (int)$aPhoto['like_count'];
            $sActions = ($iLoggId && $iOwner != $iLoggId) ? '<a href="#" class="button repinbutton">Repin</a><a href="#" class="button likebutton">Like</a>' : '';

            $aPathInfo = pathinfo($sFolder . $sFile);
            $sExt = strtolower($aPathInfo['extension']);

            $sImages .= <<<EOL
<!-- pin element {$iPhotoId} -->
<div class="pin" pin_id="{$iPhotoId}">
    <div class="holder">
        <div class="actions">
            {$sActions}
            <a href="#" class="button comment_tr">Comment</a>
        </div>
        <a class="image ajax" href="service.php?id={$iPhotoId}" title="{$sTitle}">
            <img alt="{$sTitle}" src="{$sFolder}{$sFile}">
        </a>
    </div>
    <p class="desc">{$sTitle}</p>
    <p class="info">
        <span class="LikesCount"><strong>{$iLikes}</strong> likes</span>
        <span>{$iRepins} repins</span>
        <span>{$iCmts} comments</span>
    </p>
    <form class="comment" method="post" action="" style="display: none" onsubmit="return submitComment(this, {$iPhotoId})">
        <textarea placeholder="Add a comment..." maxlength="255" name="comment"></textarea>
        <input type="submit" class="button" value="Comment" />
    </form>
</div>
EOL;
        }
        return $sImages;
    }

    // get certain photo info
    function getPhotoInfo($i) {
        $sSQL = "SELECT * FROM `pd_photos` WHERE `id` = '{$i}'";
        $aInfos = $GLOBALS['MySQL']->getAll($sSQL);
        return $aInfos[0];
    }

    // format time by timestamp
    function formatTime($iSec) {
        $sFormat = 'j F Y';
        return gmdate($sFormat, $iSec);
    }

    // insert a new blank photo into DB
    function insertBlankPhoto($sTitle, $iOwner) {
        $sTitle = $GLOBALS['MySQL']->escape($sTitle);
        $iOwner = (int)$iOwner;

        $sSQL = "INSERT INTO `pd_photos` SET `title` = '{$sTitle}', `owner` = '{$iOwner}', `when` = UNIX_TIMESTAMP()";
        $GLOBALS['MySQL']->res($sSQL);
        return $GLOBALS['MySQL']->lastId();
    }

    // update filename
    function updateFilename($i, $sFilename) {
        $sFilename = $GLOBALS['MySQL']->escape($sFilename);

        $sSQL = "UPDATE `pd_photos` SET `filename` = '{$sFilename}' WHERE `id`='{$i}'";
        return $GLOBALS['MySQL']->res($sSQL);
    }

    function acceptLike() {
        $iItemId = (int)$_POST['id']; // prepare necessary information
        $iLoggId = (int)$_SESSION['member_id'];

        if ($iItemId && $iLoggId) {
            // check - if there is any recent record from the same person for last 1 hour
            $iOldId = $GLOBALS['MySQL']->getOne("SELECT `l_item_id` FROM `pd_items_likes` WHERE `l_item_id` = '{$iItemId}' AND `l_pid` = '{$iLoggId}' AND `l_when` >= UNIX_TIMESTAMP() - 3600 LIMIT 1");
            if (! $iOldId) {
                // if everything is fine - we can add a new like
                $GLOBALS['MySQL']->res("INSERT INTO `pd_items_likes` SET `l_item_id` = '{$iItemId}', `l_pid` = '{$iLoggId}', `l_when` = UNIX_TIMESTAMP()");
                // and update total amount of likes
                $GLOBALS['MySQL']->res("UPDATE `pd_photos` SET `like_count` = `like_count` + 1 WHERE `id` = '{$iItemId}'");
            }
            // and return total amount of likes
            return (int)$GLOBALS['MySQL']->getOne("SELECT `like_count` FROM `pd_photos` WHERE `id` = '{$iItemId}'");
        }
    }
    function acceptRepin() {
        $iItemId = (int)$_POST['id']; // prepare necessary information
        $iLoggId = (int)$_SESSION['member_id'];

        if ($iItemId && $iLoggId) {
            $aPhotoInfo = $this->getPhotoInfo($iItemId);

            // check - for already repinned element
            $iOldId = $GLOBALS['MySQL']->getOne("SELECT `id` FROM `pd_photos` WHERE `owner` = '{$iLoggId}' AND `repin_id` = '{$iItemId}'");
            if (! $iOldId) {
                // if everything is fine - add a copy of photo as own photo (repin)
                $sSQL = "INSERT INTO `pd_photos` SET
                            `title` = '{$aPhotoInfo['title']}',
                            `filename` = '{$aPhotoInfo['filename']}',
                            `owner` = '{$iLoggId}',
                            `when` = UNIX_TIMESTAMP(),
                            `repin_id` = '{$iItemId}'
                ";
                $GLOBALS['MySQL']->res($sSQL);

                // update repin count for original photo
                $GLOBALS['MySQL']->res("UPDATE `pd_photos` SET `repin_count` = `repin_count` + 1 WHERE `id` = '{$iItemId}'");
            }
            // and return current member id
            return $iLoggId;
        }
    }

}

$GLOBALS['CPhotos'] = new CPhotos();

You can see, that I modified ‘getAllPhotos’ function. now it can handle with search params, plus, it displays amounts of repins and counts. Since today – repin and like buttons are available only for logged members. You can also find here two new functions ‘acceptLike’ and ‘acceptRepin’. First one is to accept likes, second one is to do ‘repin’. As you see – it just makes a single record to database (a copy of repinned object), but with a link to original photo (repin_id field).

Step 3. Javascript

I updated our main javascript file. There are only two new event handlers:

js/script.js

    // onclick event handler (for like button)
    $('.pin .actions .likebutton').click(function () {
        $(this).attr('disabled', 'disabled');

        var iPinId = $(this).parent().parent().parent().attr('pin_id');
        $.ajax({ 
          url: 'service.php',
          type: 'POST',
          data: 'add=like&id=' + iPinId,
          cache: false, 
          success: function(res){
            $('.pin[pin_id='+iPinId+'] .info .LikesCount strong').text(res);
          } 
        });
        return false;
    }); 

    // onclick event handler (for repin button)
    $('.pin .actions .repinbutton').click(function () {
        var iPinId = $(this).parent().parent().parent().attr('pin_id');
        $.ajax({ 
          url: 'service.php',
          type: 'POST',
          data: 'add=repin&id=' + iPinId,
          cache: false, 
          success: function(res){
            window.location.href = 'profile.php?id=' + res;
          } 
        });
        return false;
    });

The main idea it to use jQuery ajax to send necessary information about liked or repined photo to our ‘service.php’ server file. Once we click ‘like’ button, we send Photo ID, and then – server returns us total amount of likes for this photo, then we can update ‘like’ counter. The situation is similar for ‘repin’ button. We send photo id to server, once it ‘repins’ selected photo – it relocates us to our profile page (where we can see a result).


Live Demo

Conclusion

We have just finished our fifth lesson where we are writing our own Pinterest-like script. I hope you enjoy this series. It would be kind of you to share our materials with your friends. Good luck and welcome back!


25 COMMENTS

  1. Thanks so much sir for this tutorials am very happy u created this tutorial since my project is on image, i think i can get some ideals here to work with ;)

  2. Hello Andrew,I am a student from China.I pay attention to your site for a long time.I have learned a lot here though I am a green-hand in HTML and CSS.
    ah…My English is so poor,but I still want to show my thanks to you.
    你的这篇教程真是太棒了。在中文里人们把这种网页布局叫做“瀑布流”。
    我想将它应用在我的学校网站上,但是我一点儿也不懂SQL和PHP,我能不能把你的代码完全模仿出来?
    我会经常来这里学习的。在中国,真的很难找到这样专业而且免费的代码教学网站。
    maybe you can understand the sentences above with the help of Google translater.
    Finally,happy new year! Although there are still several days before Chinese traditional new year.

    • Hello Zhang,
      Thank you for your letter, well (I’ve translated your comments in Chinese), yes, you can use our script on your own private website.

  3. thanks for this tutorial
    is there a way to make the script runs profile page with username
    like site.com/profilename

    • Yes, sure, if you want to use permalinks, you can add it as a rule for your root .htaccess file.
      in this case, you will need to pass usernames a a GET param (for profile page)

  4. Thank You So much ,,,for givings us ur huge knowledge…..
    i learn many thiung from here infact its amazing web-site…:):):)

  5. i’v downloaded the package and installed probably, but i found an issue opening the thumb image !. it keeps loading , could you tell me what i’v to add\edit? thanks

    • Hi kosh,
      By default, it displays pre-defined avatar image (images/avatar.jpg). If you’d like to display here custom avatars for your members – you’re welcome to implement this functionality.

  6. @admin you didn’t get what i mean.
    in my previous post i asked when i launched this tutorial everything works but whenever i click on an image thumb the original image doesn’t show up, it keep loading but never show up. ! could you help me with it thanks. btw your tutorials are awesome

    • Hello kosh,
      Ah, I see, then, I think that this would be better to investigate your server response. It should be denied or time outed. Firebug should help you to understand it.

  7. Hello,
    Thanks for this material. But we will can see step 6? (follow and url upload). This is will possible?
    Sorry my english not good.

    • Hello Esteban,
      Videos? Hum, this was made especially for photos. If you need a similar solution for videos, so, it is possible too. In this case – you have to start developing it

  8. Hello Admin,
    Can you help me to add infinite scrolling to the code you have given for pinterest.
    I have tried a lot but no code is working.

  9. I’m also facing the same prob as Kosh, when I click on thumbnail to view full size of image.. it just keeps on loading.. it doesn’t even show any default image… how can I overcome this problem ?? in your demo it is working perfectly..

    • Hi Akki,
      I am curious why it so, it shouldn’t be so. Try to investigate a server response (in firebug). Maybe your server refuses ajax connections, or it generates an error in response.

      • To resolve the same probleme i have change in service.php (in html divs) the php string : “<?=" in every occurrence by this "<?php print" i and now all work fine!

  10. Great tutorial I must say, it would be more good if you add ” Create Board and add pin to the board” similar as Board in Pinterest.

  11. I continue to get these errors in any location where there is a redirect in the service.php, CMembers.php.

    Warning: Cannot modify header information – headers already sent by (output started at C:\Inetpub\vhosts\easyemailflyers.com\site3\sources328\service.php:1) in C:\Inetpub\vhosts\easyemailflyers.com\site3\sources328\service.php on line 21

    Any ideas?

Leave a Reply