How to create Pinterest-like script – step 6

Tutorials

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

1 <!-- add scripts -->
2 <script src="js/jquery.min.js"></script>
3 <script src="js/jquery.colorbox-min.js"></script>
4 <script src="js/jquery.masonry.min.js"></script>
5 <script src="js/jquery.infinitescroll.min.js"></script>
6 <script src="js/script.js"></script>

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

1 <!-- main container -->
2 <div class="main_container">
3     {images_set}
4 </div>
5 {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

1 // draw common page
2 $aKeys array(
3     '{menu_elements}' => $sLoginMenu,
4     '{extra_data}' => $sExtra,
5     '{images_set}' => $sPhotos
6 );

with next code:

01 // infinite scroll
02 $sPerpage = 20;
03 if($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') { // ajax
04     if($sPhotos) {
05         $sPage = (int)$_GET['page'] + 1;
06         echo <<<EOF
07 <div class="main_container">
08 {$sPhotos}
09 </div>
10 <nav id="page-nav">
11   <a href="index.php?page={$sPage}&per_page={$sPerpage}"></a>
12 </nav>
13 EOF;
14     }
15     exit;
16 }
17 $sInfinite = ($sPhotos == '') ? '' : <<<EOF
18 <nav id="page-nav">
19   <a href="index.php?page=2&per_page={$sPerpage}"></a>
20 </nav>
21 EOF;
22 // draw common page
23 $aKeys array(
24     '{menu_elements}' => $sLoginMenu,
25     '{extra_data}' => $sExtra,
26     '{images_set}' => $sPhotos,
27     '{infinite}' => $sInfinite
28 );

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

01 require_once('classes/CMySQL.php');
02 require_once('classes/CMembers.php');
03 require_once('classes/CPhotos.php');
04 // get login data
05 list ($sLoginMenu$sExtra) = $GLOBALS['CMembers']->getLoginData();
06 // profile id
07 $i = (int)$_GET['id'];
08 if ($i) {
09     $aMemberInfo $GLOBALS['CMembers']->getProfileInfo($i);
10     if ($aMemberInfo) {
11         // get all photos by profile
12         $sPhotos $GLOBALS['CPhotos']->getAllPhotos($i);
13         // infinite scroll
14         $sPerpage = 20;
15         if($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') { // ajax
16             if($sPhotos) {
17                 $sPage = (int)$_GET['page'] + 1;
18                 echo <<<EOF
19 <div class="main_container">
20 {$sPhotos}
21 </div>
22 <nav id="page-nav">
23   <a href="profile.php?id={$i}&page={$sPage}&per_page={$sPerpage}"></a>
24 </nav>
25 EOF;
26             }
27             exit;
28         }
29         $sInfinite = ($sPhotos == '') ? '' : <<<EOF
30 <nav id="page-nav">
31   <a href="profile.php?id={$i}&page=2&per_page={$sPerpage}"></a>
32 </nav>
33 EOF;
34         // draw profile page
35         $aKeys array(
36             '{menu_elements}' => $sLoginMenu,
37             '{extra_data}' => $sExtra,
38             '{images_set}' => $sPhotos,
39             '{profile_name}' => $aMemberInfo['first_name'],
40             '{infinite}' => $sInfinite
41         );
42         echo strtr(file_get_contents('templates/profile.html'), $aKeys);
43         exit;
44     }
45 }
46 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

01     function getAllPhotos($iPid = 0, $sKeyPar '') {
02         // prepare WHERE filter
03         $aWhere array();
04         if ($iPid) {
05             $aWhere[] = "`owner` = '{$iPid}'";
06         }
07         if ($sKeyPar != '') {
08             $sKeyword $GLOBALS['MySQL']->escape($sKeyPar);
09             $aWhere[] = "`title` LIKE '%{$sKeyword}%'";
10         }
11         $sFilter = (count($aWhere)) ? 'WHERE ' . implode(' AND '$aWhere) : '';
12         // pagination
13         $iPage = (isset($_GET['page'])) ? (int)$_GET['page'] : 1;
14         $iPerPage = (isset($_GET['per_page'])) ? (int)$_GET['per_page'] : 20;
15         $iPage = ($iPage < 1) ? 1 : $iPage;
16         $iFrom = ($iPage - 1) * $iPerPage;
17         $iFrom = ($iFrom < 1) ? 0 : $iFrom;
18         $sLimit "LIMIT {$iFrom}, {$iPerPage}";
19         $sSQL = "
20             SELECT *
21             FROM `pd_photos`
22             {$sFilter}
23             ORDER BY `when` DESC
24             {$sLimit}
25         ";
26         $aPhotos $GLOBALS['MySQL']->getAll($sSQL);
27         $sImages '';
28         $sFolder 'photos/';
29         foreach ($aPhotos as $i => $aPhoto) {
30             $iPhotoId = (int)$aPhoto['id'];
31             $sFile $aPhoto['filename'];
32             $sTitle $aPhoto['title'];
33             $iCmts = (int)$aPhoto['comments_count'];
34             $iLoggId = (int)$_SESSION['member_id'];
35             $iOwner = (int)$aPhoto['owner'];
36             $iRepins = (int)$aPhoto['repin_count'];
37             $iLikes = (int)$aPhoto['like_count'];
38             $sActions = ($iLoggId && $iOwner != $iLoggId) ? '<a href="#" class="button repinbutton">Repin</a><a href="#" class="button likebutton">Like</a>' '';
39             // display a blank image for not existing photos
40             $sFile = (file_exists($sFolder $sFile)) ? $sFile 'blank_photo.jpg';
41             $aPathInfo pathinfo($sFolder $sFile);
42             $sExt strtolower($aPathInfo['extension']);
43             $sImages .= <<<EOL
44 <!-- pin element {$iPhotoId} -->
45 <div class="pin" pin_id="{$iPhotoId}">
46     <div class="holder">
47         <div class="actions">
48             {$sActions}
49             <a href="#" class="button comment_tr">Comment</a>
50         </div>
51         <a class="image ajax" href="service.php?id={$iPhotoId}" title="{$sTitle}">
52             <img alt="{$sTitle}" src="{$sFolder}{$sFile}">
53         </a>
54     </div>
55     <p class="desc">{$sTitle}</p>
56     <p class="info">
57         <span class="LikesCount"><strong>{$iLikes}</strong> likes</span>
58         <span>{$iRepins} repins</span>
59         <span>{$iCmts} comments</span>
60     </p>
61     <form class="comment" method="post" action="" style="display: none" onsubmit="return submitComment(this, {$iPhotoId})">
62         <textarea placeholder="Add a comment..." maxlength="255" name="comment"></textarea>
63         <input type="submit" class="button" value="Comment" />
64     </form>
65 </div>
66 EOL;
67         }
68         return $sImages;
69     }

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

001 function fileSelectHandler() {
002     // get selected file
003     var oFile = $('#image_file')[0].files[0];
004     // html5 file upload
005     var formData = new FormData($('#upload_form')[0]);
006     $.ajax({
007         url: 'upload.php'//server script to process data
008         type: 'POST',
009         // ajax events
010         beforeSend: function() {
011         },
012         success: function(e) {
013             $('#upload_result').html('Thank you for your photo').show();
014             setTimeout(function() {
015                 $("#upload_result").hide().empty();
016                 window.location.href = 'index.php';
017             }, 4000);
018         },
019         error: function(e) {
020             $('#upload_result').html('Error while processing uploaded image');
021         },
022         // form data
023         data: formData,
024         // options to tell JQuery not to process data or worry about content-type
025         cache: false,
026         contentType: false,
027         processData: false
028     });
029 }
030 function submitComment(form, id) {
031     $.ajax({
032       type: 'POST',
033       url: 'service.php',
034       data: 'add=comment&id=' + id + '&comment=' + $(form).find('textarea').val(),
035       cache: false,
036       success: function(html){
037         if (html) {
038           location.reload();
039         }
040       }
041     });
042     return false;
043 }
044 function initiateColorboxHandler() {
045     $('.ajax').colorbox({
046         onOpen:function(){
047         },
048         onLoad:function(){
049         },
050         onComplete:function(){
051             $(this).colorbox.resize();
052             var iPinId = $(this).parent().parent().attr('pin_id');
053             $.ajax({
054               url: 'service.php',
055               data: 'get=comments&id=' + iPinId,
056               cache: false,
057               success: function(html){
058                 $('.comments').append(html);
059                 $(this).colorbox.resize();
060               }
061             });
062         },
063         onCleanup:function(){
064         },
065         onClosed:function(){
066         }
067     });
068 }
069 $(document).ready(function(){
070     // file field change handler
071     $('#image_file').change(function(){
072         var file = this.files[0];
073         name = file.name;
074         size = file.size;
075         type = file.type;
076         // extra validation
077         if (name && size)  {
078             if (! file.type.match('image.*')) {
079                 alert("Select image please");
080             else {
081                 fileSelectHandler();
082             }
083         }
084     });
085     // masonry initialization
086     var $container = $('.main_container');
087     $container.imagesLoaded(function(){
088       // options
089       $container.masonry({
090         itemSelector: '.pin',
091         isAnimated: true,
092         isFitWidth: true,
093         isAnimatedFromBottom: true
094       });
095     });
096     $container.infinitescroll({
097       navSelector  : '#page-nav',    // selector for the paged navigation
098       nextSelector : '#page-nav a',  // selector for the NEXT link (to page 2)
099       itemSelector : '.pin',     // selector for all items you'll retrieve
100       loading: {
101           finishedMsg: 'No more pages to load.'
102         }
103       },
104       // trigger Masonry as a callback
105       function( newElements ) {
106         // hide new items while they are loading
107         var $newElems = $( newElements ).css({ opacity: 0 });
108         // ensure that images load before adding to masonry layout
109         $newElems.imagesLoaded(function(){
110           // show elems now they're ready
111           $newElems.animate({ opacity: 1 });
112           $container.masonry( 'appended', $newElems, true );
113           // initiate colorbox
114           initiateColorboxHandler();
115         });
116       }
117     );
118     // onclick event handler (for comments)
119     $('.comment_tr').click(function () {
120         $(this).toggleClass('disabled');
121         $(this).parent().parent().parent().find('form.comment').slideToggle(400, function () {
122             $('.main_container').masonry();
123         });
124     });
125     // initiate colorbox
126     initiateColorboxHandler();
127     // onclick event handler (for like button)
128     $('.pin .actions .likebutton').click(function () {
129         $(this).attr('disabled''disabled');
130         var iPinId = $(this).parent().parent().parent().attr('pin_id');
131         $.ajax({
132           url: 'service.php',
133           type: 'POST',
134           data: 'add=like&id=' + iPinId,
135           cache: false,
136           success: function(res){
137             $('.pin[pin_id='+iPinId+'] .info .LikesCount strong').text(res);
138           }
139         });
140         return false;
141     });
142     // onclick event handler (for repin button)
143     $('.pin .actions .repinbutton').click(function () {
144         var iPinId = $(this).parent().parent().parent().attr('pin_id');
145         $.ajax({
146           url: 'service.php',
147           type: 'POST',
148           data: 'add=repin&id=' + iPinId,
149           cache: false,
150           success: function(res){
151             window.location.href = 'profile.php?id=' + res;
152           }
153         });
154         return false;
155     });
156 });

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!

Rate article