HTML5 Zone is brought to you in partnership with:

I was born in 1981 in one little city. Since I was 10y/o I programmed in different languages. My first languages were basic, then C++/MFC, after .Net (C#, VB.Net, J#, ASP.Net), XSL+XML processing). In the last 5 years I worked with web languages (HTML, CSS, PHP, SQL, XML, XSL, JavaScript). After university I worked in several different companies, eventually becoming a blogger. This is my hobby too. Andrey is a DZone MVB and is not an employee of DZone and has posted 111 posts at DZone. You can read more from them at their website. View Full User Profile

Facebook-like Photo Gallery With Comments

06.19.2012
| 29627 views |
  • submit to reddit

Facebook like photo gallery with comments

Have you thought about own facebook-style photo gallry system with comments? I have and today I prepared it for you. Main idea – when we click on images – they popup (ajax) with bigger image at the left and comments section at the right. All images are in the database (mySQL). And, of course, we will use PHP to achieve our result. Also, our comment system will prevent accepting more than 1 comment per 10 mins (to avoid spam).

Live Demo
download in package

Now – download the source files and lets start coding!

Step 1. SQL

For our gallery I prepared two SQL tables: first table keeps records of our images. It contains several fields: title, filename, description, time of adding and comments count. Another table keeps comments. So, execute next SQL instructions:

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

INSERT INTO `s281_photos` (`title`, `filename`, `description`, `when`) VALUES
('Item #1', 'photo1.jpg', 'Description of Item #1', UNIX_TIMESTAMP()),
('Item #2', 'photo2.jpg', 'Description of Item #2', UNIX_TIMESTAMP()+1),
('Item #3', 'photo3.jpg', 'Description of Item #3', UNIX_TIMESTAMP()+2),
('Item #4', 'photo4.jpg', 'Description of Item #4', UNIX_TIMESTAMP()+3),
('Item #5', 'photo5.jpg', 'Description of Item #5', UNIX_TIMESTAMP()+4),
('Item #6', 'photo6.jpg', 'Description of Item #6', UNIX_TIMESTAMP()+5),
('Item #7', 'photo7.jpg', 'Description of Item #7', UNIX_TIMESTAMP()+6),
('Item #8', 'photo8.jpg', 'Description of Item #8', UNIX_TIMESTAMP()+7),
('Item #9', 'photo9.jpg', 'Description of Item #9', UNIX_TIMESTAMP()+8),
('Item #10', 'photo10.jpg', 'Description of Item #10', UNIX_TIMESTAMP()+9);

CREATE TABLE IF NOT EXISTS `s281_items_cmts` (
  `c_id` int(11) NOT NULL AUTO_INCREMENT ,
  `c_item_id` int(12) NOT NULL default '0',
  `c_ip` varchar(20) default NULL,
  `c_name` varchar(64) default '',
  `c_text` text NOT NULL ,
  `c_when` int(11) NOT NULL default '0',
  PRIMARY KEY (`c_id`),
  KEY `c_item_id` (`c_item_id`)
) ENGINE=MYISAM DEFAULT CHARSET=utf8;

Step 2. PHP

Now, please create empty index.php file and put next code:

index.php
<?php
// disable warnings
if (version_compare(phpversion(), "5.3.0", ">=")  == 1)
  error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
else
  error_reporting(E_ALL & ~E_NOTICE); 

require_once('classes/CMySQL.php'); // include service classes to work with database and comments
require_once('classes/CMyComments.php');

if ($_POST['action'] == 'accept_comment') {
    echo $GLOBALS['MyComments']->acceptComment();
    exit;
}

// prepare a list with photos
$sPhotos = '';
$aItems = $GLOBALS['MySQL']->getAll("SELECT * FROM `s281_photos` ORDER by `when` ASC"); // get photos info
foreach ($aItems as $i => $aItemInfo) {
    $sPhotos .= '<div class="photo"><img src="images/thumb_'.$aItemInfo['filename'].'" id="'.$aItemInfo['id'].'" /><p>'.$aItemInfo['title'].' item</p><i>'.$aItemInfo['description'].'</i></div>';
}

?>
<!DOCTYPE html>
<html lang="en"><head>
    <meta charset="utf-8" />
    <title>Facebook like photo gallery with comments | Script Tutorials</title>

    <!-- Link styles -->
    <link href="css/main.css" rel="stylesheet" type="text/css" />

    <!-- Link scripts -->
    <script src="https://www.google.com/jsapi"></script>
    <script>
        google.load("jquery", "1.7.1");
    </script>
    <script src="js/script.js"></script>
</head>
<body>
    <header>
        <h2>Facebook like photo gallery with comments</h2>
        <a href="http://www.script-tutorials.com/facebook-like-photo-gallery-with-comments/" class="stuts">Back to original tutorial on <span>Script Tutorials</span></a>
    </header>

    <!-- Container with last photos -->
    <div class="container">
        <h1>Last photos:</h1>
        <?= $sPhotos ?>
    </div>

    <!-- Hidden preview block -->
    <div id="photo_preview" style="display:none">
        <div class="photo_wrp">
            <img class="close" src="images/close.gif" />
            <div style="clear:both"></div>
            <div class="pleft">test1</div>
            <div class="pright">test2</div>
            <div style="clear:both"></div>
        </div>
    </div>
</body></html>

We have just created a main index file of our gallery. By default – script generates a list of images (with title and description), and it also generates an empty hidden object which we are going to use in order to accept custom content by ajax requests. Also, when we post comments, we forward this request (to accept new comment) into comments class. Now, lets review next important php file:

photos_ajx.php
<?php
// disable warnings
if (version_compare(phpversion(), "5.3.0", ">=")  == 1)
  error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
else
  error_reporting(E_ALL & ~E_NOTICE); 

if ($_POST['action'] == 'get_info' && (int)$_POST['id'] > 0) {

    require_once('classes/CMySQL.php'); // include service classes to work with database and comments
    require_once('classes/CMyComments.php');

    // get photo info
    $iPid = (int)$_POST['id'];
    $aImageInfo = $GLOBALS['MySQL']->getRow("SELECT * FROM `s281_photos` WHERE `id` = '{$iPid}'");

    // prepare last 10 comments
    $sCommentsBlock = $GLOBALS['MyComments']->getComments($iPid);

    $aItems = $GLOBALS['MySQL']->getAll("SELECT * FROM `s281_photos` ORDER by `when` ASC"); // get photos info

    // Prev & Next navigation
    $sNext = $sPrev = '';
    $iPrev = (int)$GLOBALS['MySQL']->getOne("SELECT `id` FROM `s281_photos` WHERE `id` < '{$iPid}' ORDER BY `id` DESC LIMIT 1");
    $iNext = (int)$GLOBALS['MySQL']->getOne("SELECT `id` FROM `s281_photos` WHERE `id` > '{$iPid}' ORDER BY `id` ASC LIMIT 1");
    $sPrevBtn = ($iPrev) ? '<div class="preview_prev" onclick="getPhotoPreviewAjx(\''.$iPrev.'\')"><img src="images/prev.png" alt="prev" /></div>' : '';
    $sNextBtn = ($iNext) ? '<div class="preview_next" onclick="getPhotoPreviewAjx(\''.$iNext.'\')"><img src="images/next.png" alt="next" /></div>' : '';

    require_once('classes/Services_JSON.php');
    $oJson = new Services_JSON();
    header('Content-Type:text/javascript');
    echo $oJson->encode(array(
        'data1' => '<img class="fileUnitSpacer" src="images/'. $aImageInfo['filename'] .'">' . $sPrevBtn . $sNextBtn,
        'data2' => $sCommentsBlock,
    ));
    exit;
}

This file sends back information about requested photo. This is an enlarged image, block with comments and navigation buttons (to open previous / next images ajaxy). As you can see – we use comments class, now, it’s time to look at it too:

classes/CMyComments.php

 

<?php

class CMyComments {

    // constructor
    function CMyComments() {
    }

    // return comments block
    function getComments($i) {
        // draw last 10 comments
        $sComments = '';
        $aComments = $GLOBALS['MySQL']->getAll("SELECT * FROM `s281_items_cmts` WHERE `c_item_id` = '{$i}' ORDER BY `c_when` DESC LIMIT 10");
        foreach ($aComments as $i => $aCmtsInfo) {
            $sWhen = date('F j, Y H:i', $aCmtsInfo['c_when']);
            $sComments .= <<<EOF
<div class="comment" id="{$aCmtsInfo['c_id']}">
    <p>Comment from {$aCmtsInfo['c_name']} <span>({$sWhen})</span>:</p>
    <p>{$aCmtsInfo['c_text']}</p>
</div>
EOF;
        }

        return <<<EOF
<div class="comments" id="comments">
    <h2>Comments</h2>
    <div id="comments_warning1" style="display:none">Don`t forget to fill both fields (Name and Comment)</div>
    <div id="comments_warning2" style="display:none">You can't post more than one comment per 10 minutes (spam protection)</div>
    <form onsubmit="return false;">
        <table>
            <tr><td class="label"><label>Your name: </label></td><td class="field"><input type="text" value="" title="Please enter your name" id="name" /></td></tr>
            <tr><td class="label"><label>Comment: </label></td><td class="field"><textarea name="text" id="text"></textarea></td></tr>
            <tr><td class="label"> </td><td class="field"><button onclick="submitComment({$i}); return false;">Post comment</button></td></tr>
        </table>
    </form>
    <div id="comments_list">{$sComments}</div>
</div>
EOF;
    }

    function acceptComment() {
        $iItemId = (int)$_POST['id']; // prepare necessary information
        $sIp = $this->getVisitorIP();
        $sName = $GLOBALS['MySQL']->escape(strip_tags($_POST['name']));
        $sText = $GLOBALS['MySQL']->escape(strip_tags($_POST['text']));

        if ($sName && $sText) {
            // check - if there is any recent post from you or not
            $iOldId = $GLOBALS['MySQL']->getOne("SELECT `c_item_id` FROM `s281_items_cmts` WHERE `c_item_id` = '{$iItemId}' AND `c_ip` = '{$sIp}' AND `c_when` >= UNIX_TIMESTAMP() - 600 LIMIT 1");
            if (! $iOldId) {
                // if everything is fine - allow to add comment
                $GLOBALS['MySQL']->res("INSERT INTO `s281_items_cmts` SET `c_item_id` = '{$iItemId}', `c_ip` = '{$sIp}', `c_when` = UNIX_TIMESTAMP(), `c_name` = '{$sName}', `c_text` = '{$sText}'");
                $GLOBALS['MySQL']->res("UPDATE `s281_photos` SET `comments_count` = `comments_count` + 1 WHERE `id` = '{$iItemId}'");

                // and print out last 10 comments
                $sOut = '';
                $aComments = $GLOBALS['MySQL']->getAll("SELECT * FROM `s281_items_cmts` WHERE `c_item_id` = '{$iItemId}' ORDER BY `c_when` DESC LIMIT 10");
                foreach ($aComments as $i => $aCmtsInfo) {
                    $sWhen = date('F j, Y H:i', $aCmtsInfo['c_when']);
                    $sOut .= <<<EOF
<div class="comment" id="{$aCmtsInfo['c_id']}">
    <p>Comment from {$aCmtsInfo['c_name']} <span>({$sWhen})</span>:</p>
    <p>{$aCmtsInfo['c_text']}</p>
</div>
EOF;
                }
                return $sOut;
            }
        }
        return 1;
    }

    // get visitor IP
    function getVisitorIP() {
        $ip = "0.0.0.0";
        if( ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) && ( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) ) {
            $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
        } elseif( ( isset( $_SERVER['HTTP_CLIENT_IP'])) && (!empty($_SERVER['HTTP_CLIENT_IP'] ) ) ) {
            $ip = explode(".",$_SERVER['HTTP_CLIENT_IP']);
            $ip = $ip[3].".".$ip[2].".".$ip[1].".".$ip[0];
        } elseif((!isset( $_SERVER['HTTP_X_FORWARDED_FOR'])) || (empty($_SERVER['HTTP_X_FORWARDED_FOR']))) {
            if ((!isset( $_SERVER['HTTP_CLIENT_IP'])) && (empty($_SERVER['HTTP_CLIENT_IP']))) {
                $ip = $_SERVER['REMOTE_ADDR'];
            }
        }
        return $ip;
    }
}

$GLOBALS['MyComments'] = new CMyComments();

?>
 

This class performs two main functions – it can accept new comments and also it can give us a box with comments. There are two more service classes: CMySQL.php and Services_JSON.php. They are two known classes to work with database and json. You can adjust database settings in database class. Both classes available in our package.

Step 3. Javascript

Now we should prepare user interface behavior with using javascript, please prepare next file for the project: js/script.js
// close photo preview block
function closePhotoPreview() {
    $('#photo_preview').hide();
    $('#photo_preview .pleft').html('empty');
    $('#photo_preview .pright').html('empty');
};

// display photo preview block
function getPhotoPreviewAjx(id) {
    $.post('photos_ajx.php', {action: 'get_info', id: id},
        function(data){
            $('#photo_preview .pleft').html(data.data1);
            $('#photo_preview .pright').html(data.data2);
            $('#photo_preview').show();
        }, "json"
    );
};

// submit comment
function submitComment(id) {
    var sName = $('#name').val();
    var sText = $('#text').val();

    if (sName && sText) {
        $.post('index.php', { action: 'accept_comment', name: sName, text: sText, id: id },
            function(data){
                if (data != '1') {
                    $('#comments_list').fadeOut(1000, function () {
                        $(this).html(data);
                        $(this).fadeIn(1000);
                    });
                } else {
                    $('#comments_warning2').fadeIn(1000, function () {
                        $(this).fadeOut(1000);
                    });
                }
            }
        );
    } else {
        $('#comments_warning1').fadeIn(1000, function () {
            $(this).fadeOut(1000);
        });
    }
};

// init
$(function(){

    // onclick event handlers
    $('#photo_preview .photo_wrp').click(function (event) {
        event.preventDefault();

        return false;
    });
    $('#photo_preview').click(function (event) {
        closePhotoPreview();
    });
    $('#photo_preview img.close').click(function (event) {
        closePhotoPreview();
    });

    // display photo preview ajaxy
    $('.container .photo img').click(function (event) {
        if (event.preventDefault) event.preventDefault();

        getPhotoPreviewAjx($(this).attr('id'));
    });
})

Please note, we use jQuery instructions in our script (I hope that you haven’t forgot that we linked jQuery library in the header section through google service).

Step 4. CSS

In the long run, we should stylize our page elements (our container with photos, photo preview area with comments):

css/main.css
/* project styles */
.container {
    border: 1px solid #111111;
    color: #000000;
    margin: 20px auto;
    overflow: hidden;
    padding: 15px;
    position: relative;
    text-align: center;
    width: 1090px;

    -moz-border-radius: 5px;
    -ms-border-radius: 5px;
    -o-border-radius: 5px;
    -webkit-border-radius: 5px;
    border-radius: 5px;
}
.photo {
    border: 1px solid transparent;
    float: left;
    margin: 4px;
    overflow: hidden;
    padding: 4px;
    white-space: nowrap;

    /* CSS3 Box sizing property */
    -moz-box-sizing: border-box;
    -webkit-box-sizing: border-box;
    -o-box-sizing: border-box;
    box-sizing: border-box;

    /* CSS3 transition */
    -moz-transition: border 0.2s ease 0s;
    -ms-transition: border 0.2s ease 0s;
    -o-transition: border 0.2s ease 0s;
    -webkit-transition: border 0.2s ease 0s;
    transition: border 0.2s ease 0s;
}
.photo:hover {
    border-color: #444;
}
.photo img {
    cursor: pointer;
    width: 200px;
}
.photo p, .photo i {
    display: block;
}
.photo p {
    font-weight: bold;
}

/* preview styles */
#photo_preview {
    background-color: rgba(0, 0, 0, 0.7);
    bottom: 0;
    color: #000000;
    display: none;
    left: 0;
    overflow: hidden;
    position: fixed;
    right: 0;
    top: 0;
    z-index: 10;
}
.photo_wrp {
    background-color: #FAFAFA;
    height: auto;
    margin: 100px auto 0;
    overflow: hidden;
    padding: 15px;
    text-align: center;
    vertical-align: middle;
    width: 1000px;

    -moz-border-radius: 5px;
    -ms-border-radius: 5px;
    -o-border-radius: 5px;
    -webkit-border-radius: 5px;
    border-radius: 5px;
}
.close {
    cursor: pointer;
    float: right;
}
.pleft {
    float: left;
    overflow: hidden;
    position: relative;
    width: 600px;
}
.pright {
    float: right;
    position: relative;
    width: 360px;
}
.preview_prev, .preview_next {
    cursor: pointer;
    margin-top: -64px;
    opacity: 0.5;
    position: absolute;
    top: 50%;

    -moz-transition: opacity 0.2s ease 0s;
    -ms-transition: opacity 0.2s ease 0s;
    -o-transition: opacity 0.2s ease 0s;
    -webkit-transition: opacity 0.2s ease 0s;
    transition: opacity 0.2s ease 0s;
}
.preview_prev:hover, .preview_next:hover {
    opacity: 1;
}
.preview_prev {
    left: 20px;
}
.preview_next {
    right: 40px;
}

/* comments styles */
#comments form {
    margin: 10px 0;
    text-align: left;
}
#comments table td.label {
    color: #000;
    font-size: 13px;
    padding-right: 3px;
    text-align: right;
    width: 105px;
}
#comments table label {
    color: #000;
    font-size: 16px;
    font-weight: normal;
    vertical-align: middle;
}
#comments table td.field input, #comments table td.field textarea {
    border: 1px solid #96A6C5;
    font-family: Verdana,Arial,sans-serif;
    font-size: 16px;
    margin-top: 2px;
    padding: 6px;
    width: 250px;
}
#comments_list {
    margin: 10px 0;
    text-align: left;
}
#comments_list .comment {
    border-top: 1px solid #000;
    padding: 10px 0;
}
#comments_list .comment:first-child {
    border-top-width:0px;
}
#comments_list .comment span {
    font-size: 11px;
}

And...You're Done

Published at DZone with permission of Andrey Prikaznov, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)