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!
Hi Andrey,
Thank you so much for share with us. thank’s.
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.
Hi Sam, can you explain what rows you mean? In description of photos?
very nice
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.