How to create Pinterest-like script – step 6

How to create Pinterest-like script – step 6

58 351000
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

[sociallocker]

download in package

[/sociallocker]


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!

SIMILAR ARTICLES

Understanding Closures

0 24630

58 COMMENTS

  1. Is there a way to generate Google Adsense ads within the rows? This would a really cool and efficient way to display ads without being too intrusive.

  2. Hi, this is a great tutorial. i have one little issue, how to add a custom title for an image, at the moment it is drawing the image name as the title.

    • Hi Pieter, in order to implement this feature, you will need to customize the code. Right after you accepted image, you can display an easy form (additional) where you can put custom title. After you submit this second form, it should update image title.

Leave a Reply