How to create Pinterest-like script – step 6

How to create Pinterest-like script – step 6

58 48990
How to create Pinterest-like script - step 6
How to create Pinterest-like script - step 6

How to create Pinterest-like script – step 6

Several our readers asked us to implement an infinite scroll for our Pinterest script, thus I decided to implement it today. I made some research, and came to http://www.infinite-scroll.com/. I think that the library is ideal for the realization of our objectives. It let us make some kind of endless pages. It means that initially we can render a certain amount of images, when we want to see more images, we can easily scroll down, and new set of images will be loaded ajaxy. If you are ready – let’s start.

You are welcome to try our updated demo and download the fresh source package:

Live Demo

Step 1. HTML

The first thing you have to do is – to download the plugin jquery.infinitescroll.min.js and put it into your ‘js’ directory. Now, we can link this new library in the header of our ‘templates/index.html’, now, full list of attached libraries looks like:

templates/index.html

    <!-- add scripts -->
    <script src="js/jquery.min.js"></script>
    <script src="js/jquery.colorbox-min.js"></script>
    <script src="js/jquery.masonry.min.js"></script>
    <script src="js/jquery.infinitescroll.min.js"></script>
    <script src="js/script.js"></script>

Another small change in this file – a new template key (in the end of main container) – {infinite}

    <!-- main container -->
    <div class="main_container">
        {images_set}
    </div>
    {infinite}

Exactly the same changes we have to repeat in ‘templates/profile.html’ file (in our plans is to add infinite scroll for both: index and profile pages)

Step 2. PHP

Well, in the previous step we prepared our template files, now – we have to exchange our {infinite} key for a certain value. The first file is index page. Please replace our previous set of template keys

index.php

// draw common page
$aKeys = array(
    '{menu_elements}' => $sLoginMenu,
    '{extra_data}' => $sExtra,
    '{images_set}' => $sPhotos
);

with next code:

// infinite scroll
$sPerpage = 20;

if($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') { // ajax
    if($sPhotos) {
        $sPage = (int)$_GET['page'] + 1;
        echo <<<EOF
<div class="main_container">
{$sPhotos}
</div>
<nav id="page-nav">
  <a href="index.php?page={$sPage}&per_page={$sPerpage}"></a>
</nav>
EOF;
    }
    exit;
}

$sInfinite = ($sPhotos == '') ? '' : <<<EOF
<nav id="page-nav">
  <a href="index.php?page=2&per_page={$sPerpage}"></a>
</nav>
EOF;

// draw common page
$aKeys = array(
    '{menu_elements}' => $sLoginMenu,
    '{extra_data}' => $sExtra,
    '{images_set}' => $sPhotos,
    '{infinite}' => $sInfinite
);

The main idea – to get fresh data each time we request this page (of course, it depends on next GET params: ‘page’ and ‘per_page’). The similar changes I prepared for our profile page, look at the fresh version:

profile.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();

// profile id
$i = (int)$_GET['id'];
if ($i) {
    $aMemberInfo = $GLOBALS['CMembers']->getProfileInfo($i);
    if ($aMemberInfo) {

        // get all photos by profile
        $sPhotos = $GLOBALS['CPhotos']->getAllPhotos($i);

        // infinite scroll
        $sPerpage = 20;

        if($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') { // ajax
            if($sPhotos) {
                $sPage = (int)$_GET['page'] + 1;
                echo <<<EOF
<div class="main_container">
{$sPhotos}
</div>
<nav id="page-nav">
  <a href="profile.php?id={$i}&page={$sPage}&per_page={$sPerpage}"></a>
</nav>
EOF;
            }
            exit;
        }

        $sInfinite = ($sPhotos == '') ? '' : <<<EOF
<nav id="page-nav">
  <a href="profile.php?id={$i}&page=2&per_page={$sPerpage}"></a>
</nav>
EOF;

        // draw profile page
        $aKeys = array(
            '{menu_elements}' => $sLoginMenu,
            '{extra_data}' => $sExtra,
            '{images_set}' => $sPhotos,
            '{profile_name}' => $aMemberInfo['first_name'],
            '{infinite}' => $sInfinite
        );
        echo strtr(file_get_contents('templates/profile.html'), $aKeys);
        exit;
    }
}

header('Location: error.php');

Pay attention, that by default we display 20 images per page. My final touches were in the main Photos class (CPhotos.php). As you remember, we had to operate with two new GET params for pagination: ‘page’ and ‘per_page’. I added the processing of both parameters in the function ‘getAllPhotos’:

classes/CPhotos.php

    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) : '';

        // pagination
        $iPage = (isset($_GET['page'])) ? (int)$_GET['page'] : 1;
        $iPerPage = (isset($_GET['per_page'])) ? (int)$_GET['per_page'] : 20;
        $iPage = ($iPage < 1) ? 1 : $iPage;
        $iFrom = ($iPage - 1) * $iPerPage;
        $iFrom = ($iFrom < 1) ? 0 : $iFrom;
        $sLimit = "LIMIT {$iFrom}, {$iPerPage}";

        $sSQL = "
            SELECT * 
            FROM `pd_photos`
            {$sFilter}
            ORDER BY `when` DESC
            {$sLimit}
        ";

        $aPhotos = $GLOBALS['MySQL']->getAll($sSQL);

        $sImages = '';
        $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>' : '';

            // display a blank image for not existing photos
            $sFile = (file_exists($sFolder . $sFile)) ? $sFile : 'blank_photo.jpg';

            $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;
    }

As you can see, both params affect SQL limits only.

Step 3. Javascript

Final changes I made in the main javascript file. There are only two new event handlers:

js/script.js

function fileSelectHandler() {
    // get selected file
    var oFile = $('#image_file')[0].files[0];

    // html5 file upload
    var formData = new FormData($('#upload_form')[0]);
    $.ajax({
        url: 'upload.php', //server script to process data
        type: 'POST',
        // ajax events
        beforeSend: function() {
        },
        success: function(e) {
            $('#upload_result').html('Thank you for your photo').show();

            setTimeout(function() {
                $("#upload_result").hide().empty();
                window.location.href = 'index.php';
            }, 4000);
        },
        error: function(e) {
            $('#upload_result').html('Error while processing uploaded image');
        },
        // form data
        data: formData,
        // options to tell JQuery not to process data or worry about content-type
        cache: false,
        contentType: false,
        processData: false
    });
}

function submitComment(form, id) {
    $.ajax({ 
      type: 'POST',
      url: 'service.php',
      data: 'add=comment&id=' + id + '&comment=' + $(form).find('textarea').val(),
      cache: false, 
      success: function(html){
        if (html) {
          location.reload();
        }
      } 
    });
    return false;
}

function initiateColorboxHandler() {
    $('.ajax').colorbox({
        onOpen:function(){
        },
        onLoad:function(){
        },
        onComplete:function(){
            $(this).colorbox.resize();
            var iPinId = $(this).parent().parent().attr('pin_id');
            $.ajax({ 
              url: 'service.php',
              data: 'get=comments&id=' + iPinId,
              cache: false, 
              success: function(html){
                $('.comments').append(html);

                $(this).colorbox.resize();
              } 
            });

        },
        onCleanup:function(){
        },
        onClosed:function(){
        }
    });
}

$(document).ready(function(){

    // file field change handler
    $('#image_file').change(function(){
        var file = this.files[0];
        name = file.name;
        size = file.size;
        type = file.type;

        // extra validation
        if (name && size)  {
            if (! file.type.match('image.*')) {
                alert("Select image please");
            } else {
                fileSelectHandler();
            }
        }
    });

    // masonry initialization
    var $container = $('.main_container');
    
    $container.imagesLoaded(function(){
      // options
      $container.masonry({
        itemSelector: '.pin',
        isAnimated: true,
        isFitWidth: true,
        isAnimatedFromBottom: true
      });
    });
    
    $container.infinitescroll({
      navSelector  : '#page-nav',    // selector for the paged navigation 
      nextSelector : '#page-nav a',  // selector for the NEXT link (to page 2)
      itemSelector : '.pin',     // selector for all items you'll retrieve
      loading: {
          finishedMsg: 'No more pages to load.'
        }
      },
      // trigger Masonry as a callback
      function( newElements ) {
        // hide new items while they are loading
        var $newElems = $( newElements ).css({ opacity: 0 });
        // ensure that images load before adding to masonry layout
        $newElems.imagesLoaded(function(){
          // show elems now they're ready
          $newElems.animate({ opacity: 1 });
          $container.masonry( 'appended', $newElems, true ); 

          // initiate colorbox
          initiateColorboxHandler();
        });
      }
    );

    // onclick event handler (for comments)
    $('.comment_tr').click(function () {
        $(this).toggleClass('disabled');
        $(this).parent().parent().parent().find('form.comment').slideToggle(400, function () {
            $('.main_container').masonry();
        });
    }); 

    // initiate colorbox
    initiateColorboxHandler();

    // 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;
    });
});

As you remember, in the first step we added a new jQuery library: infinitescroll. I added initialization of infinitescroll library here (for our infinite scroll) and modified initialization of masonry. Because we have to sort the new images, plus we have to handle onclick event for all new images (colorbox).


Live Demo

Conclusion

We have just finished our sixth 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!


58 COMMENTS

  1. Hi,

    Andrew this series of tutorial is so great, you are so cool to share something like this.
    Realy a big man, complete and awesome tutorial with a lot of things to learn !
    I don’t believe it’s to great :) !

    I’am very happy to have find this site and glad to see people share knowledge like you did, thx for all of us.

  2. Hello
    I like your pinterest script but its not complete version with backend. Could you please do a full working clone of pinterest script as the same as pinterest.com in a lower coast please.
    Thanks

    • Hi Neon,
      It is already ready as a module for Dolphin CMS, or, you are looking to a stand alone script with own admin panel etc? And, what price do you hope to get? Ten dollar, one hundred, thousand?

  3. Hi,

    This tutorial is too good and given a step by step instructions on how to develop pinterest style websites.

    Thank you so much.

    One request, can we have responsive design in place?

    • Hello Venu, frankly speaking, it is already nearly responsive, because it re-sorts all the photos depending on a browser resolution, just try to resize your browser size to see it in action. This is the result of ‘colorbox’ plugin.

  4. I’am very happy to have find this site and glad to see people share knowledge like you did, thx for all of us.

  5. Dalai Lama once said, “Share your Knowledge. It’s a way to achieve immortality.” I think you have listened to him; your sharing of knowledge is appreciable. Especially for computer science, as this is age of IT, your tutorials and articles are helpful for many people in IT field.

  6. hye there. thanks for this wonderful work and its totally free i couldn’t even believe at first that it was free. thanks for your sharing.
    i would like to ask you question. does it have admin page ? as i can see in the database , there is an admin email over there. but couldn’t login with it as i dont knw the password for it. does it have admin panel ? where can delete photos ?

    • Hi Kosh, I haven’t implemented an admin panel till this version. But, I had envisaged this case. Please notice, that there is the ‘role’ field in the profiles table. This is the way how you can expand the functionality of this module and implement admins there, with more right and possibilities.

  7. I’m new to php and sql and I was wondering if I need to create tables in my database before hand or does the script do it for me?
    Thanks for any help.

  8. Hi Andrew,
    I just downloaded the package from this page and when I run index.html from the template folder,I get the main page devoid of all styles and appears plain.Any help will be appreciated. Attached image:
    http://imgur.com/0hn4SZi

    • Hello,
      You don’t need to run any template files, but you should open the ‘index.php’ (which is in the root folder)

  9. Hi Andrew this is Ravi,

    I tried your script in my server hoping to making a portfolio page .(I am not a professional web developer but Interested to learn)
    I have imported sql.sql file through php admin and hosted all the file into server with some minor changes in index.html page. I can’t establish login page but profile details are reflected in database for the first attempt and if tried with new email id it showing login first.
    raviadusumilli.com/designcloud is link to my page

    Thanks for the Tutorial
    Have a nice time:)

    • Hello Ravi,
      Make sure that you linked your version properly with MySQL (config params are in the ‘CMySQL.php’)

  10. pls can you add a simple fix to delete photos? so each user can delete their own photos?

    That’s all needed.. pls

    • Hello Matias,
      You can add this functionality to the profile page (for example). Useful code snippets:
      if ($i == $_SESSION['member_id'] && $_SESSION['member_status'] == ‘active’) {
      // display delete buttons
      }
      And, in order to remove a picture, you can use php’s ‘unlink’ command.

  11. Hello Admin,

    First of all many thanks for the great code. Am learning now.

    Can you please tell me how to add a close(X) button in the right corner after the comment button and help me to write script of closing the whole block while clicking the (X) icon?

    Thanks,
    Mani

    • Hi Mani,
      You can add your own custom Close button whenever you want, you can use this JS to close colorbox’s popup: $.colorbox.close();

    • Hi Sam again,
      Unfortunately, categories were not developed in this manual. They may be developed in the future

  12. HI Andrew

    well written
    i am trying to implement this in one of my site
    its working. but the problem is that i am loading the pin division through ajax.
    after loading though ajax its not working properly, the column spacing is not correct.
    is there any thing to do when we load the pin division through ajax

    • Hello Santhosh,
      I think that you will need to re-call $container.masonry once your Ajax is completed.

  13. Hey Andrew,

    Thanks for the tutorial! I have learned a lot so far. I was wondering how to make the “Likes” page that is similar to “Profile.php” except for instead of showing repined photos it shows all the photos that were liked by the user. I imagine that it would find all of the “l_id”s that were liked by the “l_pid” who is logged in. Then it would display all the photos with those “l_id”s from pd_photos? Could you help get me started with the logic and code behind this?

    • Hi Like,
      Yes, you are right. Basically, you can refer to the `pd_items_likes` table, and fetch rows WHERE `l_pid` = ‘{$iMemberID}’
      Where $iMemberID is ID of member whose photos you need to display. And then, you will be able to get all other fields (photo info) from the `pd_photos` table.
      By the way, there is the ready function to get photo info:
      CPhotos::getPhotoInfo

    • Hi Tagecho,
      Title is already here. It takes a photo filename automatically. If you need to add the new field (URL), and (or) to make the Title field editable – you will need to customize the script and add these two fields to the ‘add pin’ form.

  14. I like your demo of Pinterest-like script – step 6, but the same is in PHP and our website is in asp.net, If you have a copy of same with asp.net syntax and code then kindly share with me.

    Thanking you,

    Kamlesh

    • Hello Kamlesh,
      Unfortunately I don’t have any ready codes for ASP.NET. However, you may use the Response.Write Method for HTTP output: http://msdn.microsoft.com/en-us/library/ms525585.aspx

  15. alright mate love the pinterest tutorial? is there anyway showing how to function to let users upload other files such as mp3s and videos?

    • Hi Kevin,
      I’m afraid that this is already off-topic. Our tutorial is about working with images only.

  16. Hi I really love this tutorial, but it can not access the gallery of filemanager of a phone only the camera

    • Hello Derrick,
      Is it related to our tutorial? Because there are no any special ties with phone’s settings

  17. Enlarging functionality is not working, i read all the above comments and u disaggred with this issue but really enlarging is not working fine, please correct it and when i look in console it says “Uncaught SyntaxError: Unexpected token < index.php:6
    Failed to load resource: the server responded with a status of 403 (Forbidden) http://localhost/source_5/sources342/%3C?=%20$sFullImgPath%20?%3E&quot;

    • Hi Olly,
      Then why it works for me? It works in both my browsers (FF + Chrome), I’ve just re-checked it again – and it works. It works good at localhost too. By the way – there is no any code in line 6 of the index.php file.

Leave a Reply