facebook
Bob at Genuitec
Virtual evangelist at large. The face of Genuitec often appearing in graphics. Pen-name for our more shy writers of content.
Posted on Jan 8th 2018

In this project, we are going to create a Twitter clone using HTML, CSS and jQuery only. We will create the Registration screen and Twitter wall, where you can post tweets up to 250 characters in length. You can also retweet and like the tweets. However, since we are not using any server and database, nothing will persist. As soon as you reload the page, everything will be lost. The main purpose of this article is to show you how to create front end using client side scripting. We will use Webclipse to create our project. Let’s get started.

This project can also be viewed on Github.

Live demo

Here is what the Twitter clone app will look like:

Creating a JavaScript (jQuery) Project in Webclipse

Start Webclipse on your computer system. Choose File  > New > JavaScript Project.

creating-twitter-clone-ss-1

Enter a name for your project. We are using `twitterclone` as shown in the image.

creating-twitter-clone-ss-2

Click Finish, and your project will be created.

Need to create a front end using client side scripting? Why not try our Webclipse, an Eclipse plug-in that will change the way you work. You will love its syntax highlighting, content assist, instant validation, and more! Angular IDE, CodeLive, Emmet, JSjet, a debugger for JavaScript and TypeScript are also part of it, not to mention enhanced navigation and code sharing for Slack.

Creating HTML and CSS Files for Twitter Clone

We are going to create the registration screen and wall for Twitter Clone. If you check on Twitter website, there are mainly 3 fields in their registration form – Full Name, E-mail and Password. In our Registration file, we will make four fields – Full Name, E-mail, Password and User ID. User IDs or Usernames are the essential part of Twitter system, just like hashtags. In our application, we are not providing any distinct support for hashtags or urls.

Since there is no server side scripting and database involved in this project, there is no meaning of login screen. Therefore, we are using the term registration and login interchangeably.

Let’s first create the `login.html` file. Right click on the `twitterclone` directory in the project explorer window and select New > File (as shown on the image below).

creating-twitter-clone-ss-3

In the next window, you will be prompted to enter the file name. Here we are providing `login.html` as our file name. Click Finish after entering the file name.

creating-twitter-clone-ss-4

In the `login.html` file, we are going to insert the HTML code for the registration form. Here is the complete code for the registration screen:

<!DOCTYPE html>
 <html>
     <head>
         <meta charset="utf-8">
         link href="https://necolas.github.io/normalize.css/7.0.0/normalize.css" rel="stylesheet">
         <link href="index.css" rel="stylesheet">
         <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
         <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
         <script
             src="https://code.jquery.com/jquery-3.2.1.min.js"
             integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
             crossorigin="anonymous"></script>
         <script src="login.js"></script>
     </head>
     <body class="segoe">
         <div class="topcontainer">
             <div class="topbar">
                 <i class="fa fa-twitter logocenter" aria-hidden="true"></i>
             </div>

             <div class="AppContent wrapper wrapper-signup" id="page-container">
                 <link rel="stylesheet" href="https://abs.twimg.com/a/1511833274/css/t1/t1_signup.bundle.css">
                 <div class="page-canvas">
                     <div class="signup-wrapper">
                         <h1>
                             Join Twitter Clone today.
                         </h1>
                         <div class="t1-form signup " id="phx-signup-form">
                             <div class="textbox">
                                 <div class="prompt name">
                                     <div data-fieldname="name" class="field">
                                         <div class="sidetip">
                                             <p id="nameerror" role="alert" class="blank invalid error">What's your name?</p>
                                         </div>
                                         <input type="text" placeholder="Full name" aria-required="true" maxlength="50" id="full-name" class="">
                                     </div>
                                 </div>

                                 <div class="prompt name">
                                     <div data-fieldname="name" class="field">
                                         <div class="sidetip">
                                             <p id="uiderror" role="alert" class="blank invalid error">Write a user id</p>
                                         </div>
                                         <input type="text" placeholder="User Id" aria-required="true" maxlength="20" id="uid" class="">
                                     </div>
                                 </div>

                                 <div class="prompt email">
                                     <div data-fieldname="email" class="field">
                                         <div class="sidetip">
                                             <p id="emailerror" role="alert" class="invalid error">Please enter a valid email.</p>
                                         </div>
                                         <input type="text" placeholder="Email" aria-required="true" class="email-input" id="email">
                                     </div>
                                 </div>
                                 <div class="prompt password">
                                     <div data-fieldname="password" class="field">
                                         <div class="sidetip">
                                             <p id="passerror" role="alert" class="blank error">Please enter a password.</p>
                                         </div>
                                         <input type="password" placeholder="Password" aria-required="true" id="password">
                                     </div>
                                 </div>
                             </div>
                             <div class="doit">
                                 <div class="sign-up-box">
                                     <input type="submit" value="Sign up" id="submit_button" class="signup EdgeButton EdgeButton--primary EdgeButton--large submit">
                                 </div>
                             </div>
                         </div>
                     </div>
                 </div>
             </div>
         </div>
     </body>
 </html>

The file will look like the image below:

creating-twitter-clone-ss-5

Here in this file, Webclipse is showing some validation issues. You can see three yellow triangles, which states that something about this file could be improved. Nothing is wrong with the code, but we can improve the performance by loading the stylesheet in the head section of the file.

Similarly, we need to create a stylesheet and one more HTML file. Name these files as `index.css` and `profile.html`.

The code for `profile.html` is given below:

<!DOCTYPE html>
<html>
     <head>
         <meta charset="utf-8">
         <title>Twitter Wall</title>
         <link href="https://necolas.github.io/normalize.css/7.0.0/normalize.css" rel="stylesheet">
         <link href="index.css" rel="stylesheet">
         <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
         <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
 
         <script
             src="https://code.jquery.com/jquery-3.2.1.min.js"
             integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
             crossorigin="anonymous"></script>
         <script src="profile.js"></script> 
     </head>
     <body class="segoe" style="background: rgb(230, 236, 240);">
         <div>
             <div class="wallcontainer">
                 <div class="profilecard">
                     <div class="profilecardhead"></div>
                     <div class="profilecardimagediv">
                         <img src="http://3.bp.blogspot.com/-JCYefwq__2U/TxCfC3s1ZpI/AAAAAAAAKcM/u5mw7qPAL0w/s300-c/Camilla-Belle-6.jpg">
                     </div>

                     <div class="profilecardnameidcont">
                         <div class="profilecardnamecont">
                             <span id="profilecardname"></span>
                         </div>
                         <span id="profilecarduid"></span>
                     </div>

                     <div class="profilecardstatsdiv">
                         <ul class="profilecardstatslist">
                             <li class="profilecardstatslistitem">
                                 <span class="dispblk">
                                     <span class="statslistitemhead">Tweets</span>
                                     <span id="statslistitemcount" class="statslistitemcount">0</span>
                                 </span>
                             </li>

                             <li class="profilecardstatslistitem">
                                 <span class="dispblk">
                                     <span class="statslistitemhead">Following</span>
                                     <span class="statslistitemcount">35</span>
                                 </span>
                             </li>

                             <li class="profilecardstatslistitem">
                                 <span class="dispblk">
                                     <span class="statslistitemhead">Followers</span>
                                     <span class="statslistitemcount">34</span>
                                 </span>
                             </li>
                         </ul>
                     </div>
                 </div>


                 <div class="rightcontainer">
                     <div class="posttweetcontainer">
                         <img class="posttweetprofimg" src="http://3.bp.blogspot.com/-JCYefwq__2U/TxCfC3s1ZpI/AAAAAAAAKcM/u5mw7qPAL0w/s300-c/Camilla-Belle-6.jpg">
                         <div class="ml56px">
                             <div class="posttweettacontainer">
                                 <textarea id="posttweetta" class="posttweetta" placeholder="What's happening?"></textarea>
                                 <div class="posttweetcountcont">
                                     <span class="posttweetcount"><span id="totalchars">0</span>/250</span>
                                 </div>
                             </div>
                             <div class="posttweetbutcont">
                                 <button id="posttweetbut" class="posttweetbut">Tweet</button>
                             </div>
                         </div>
                     </div>
                     <div>
                         <ul id="tweetscontainer" class="tweetscontainer">
 
                         </ul>
                     </div>
                 </div>
             </div>
         </div>
     </body>
</html>

And the code for `index.css` is as follows:

body {
 color: #14171a;
 font-size: 14px;
 line-height: 20px;
}

body {
 font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
}


.topbar {
 background-image: url("https://abs.twimg.com/a/1512226556/img/t1/lohp_streams_header_bg_v4.png");
 background-size: 100% 100%;
 border-bottom: 1px solid rgba(0, 0, 0, 0.25);
 height: 46px;
 position: relative;
 width: 100%;
 text-align:center;
}

body.segoe {
 font-family: "Segoe UI",Arial,sans-serif;
}


.topbar .container {
 max-width: 1190px;
 height: 100%;
 text-align: center;
 width: auto;
 margin: 0 auto;
 position: relative;
}

.logocenter{
 font-size: 23px !important;
 margin-top: 11px;
 color:white;
}

.topcontainer{
 background:#FFF;
 height: 100%;
}

#submit_button {
 background-color: #1da1f2;
 border: 1px solid #1da1f2;
 border-radius: 100px;
 box-shadow: none;
 color: #fff;
 font-size: 18px;
 font-weight: bold;
 height: auto;
 line-height: 24px;
 padding: 12px 20px;
 position: relative;
 text-align: center;
 white-space: nowrap;
 width: 392px;
}

.wallcontainer{
 width: 900px; overflow: hidden; margin:auto;
}

.profilecard{
 width: 290px; float: left; background: #FFF; margin: 10px; position: relative;
}

.profilecardhead{
 background-color:#ff0000;height:95px;
}

.profilecardimagediv{
 margin:-30px 0 0 8px;padding:1px;background:#FFF;display:inline-block;border-radius:50%;
}

.profilecardimagediv img{
 width:72px;height:72px;border-radius:50%;box-sizing:border-box;border:2px solid #FFF;
}

.profilecardnameidcont{
 position:absolute;top:103px;left:90px;width:185px;
}

.profilecardnamecont{
 font-size:18px;font-weight:bold;line-height:25px;margin:-1px 0 -2px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;word-wrap:normal
}

#profilecardname{
 color:inherit;text-transform:uppercase;
}

#profilecarduid{
 color:#667786;font-size:14px;padding-right:5px;text-transform:uppercase;
}

.profilecardstatsdiv{
 padding:16px;
}

.profilecardstatslist{
 table-layout:fixed;box-sizing:border-box;display:table;margin:0;min-width:100%;padding:0;list-style:outside none;
}

.profilecardstatslistitem{
 width:1%;vertical-align:bottom;display:table-cell;padding:0;box-sizing:border-box;line-height:1;overflow:hidden;transition:all 0.15s ease-in-out 0s;text-align:inherit;
}

.dispblk{
 display:block;
}

.statslistitemhead{
 color:#657786;font-size:12px;font-weight:bold;letter-spacing:0,02em;line-height:16px;overflow:hidden;transition:color 0.15s ease-in-out 0s;display:block;
}

.statslistitemcount{
 display:block;font-size:18px;font-weight:bold;padding-top:3px;transition: color .15s ease-in-out 0s;color:#ff0000;
}

.rightcontainer{
 width:580px;float:right;margin-top:10px;margin-right:10px;
}

.posttweetcontainer{
 background:#ffe5e5;padding:10px 12px;position:relative;
}

.posttweetprofimg{
 width:32px;height:32px;position:absolute;left:28px;top:13px;border-radius:50%;
}

.ml56px{
 margin-left:56px;
}

.ml58px{
 margin-left:58px;
}

.mt10px{
 margin-top: 10px;
}

.posttweettacontainer{
 border-radius: 8px; border: 1px solid rgb(255, 153, 153); border-image: none; line-height: 20px; box-shadow: 0px 0px 0px 1px #ff9999; background-color: rgb(255, 255, 255);
}

.posttweetta{
 border-width: 1px 1px 0px; border-style: solid solid none; border-color: rgb(230, 236, 240) rgb(230, 236, 240) currentColor; margin: 0px; padding: 8px; outline: 0px; border-radius: 8px; border-image: none; width: 100%; height: 50px; font-family: "Segoe UI",Arial,sans-serif; font-size: 14px; box-sizing: border-box; opacity: 0.8; background-color: rgb(255, 255, 255);
}

.posttweetcountcont{
 overflow: hidden; margin-top: 10px;
}

.posttweetcount{
 margin-right: 10px; display: block; float: right;
}

.posttweetbutcont{
 overflow: hidden; margin-top: 10px;
}

.posttweetbut{
 padding: 6px 16px; border-radius: 100px; border: 1px solid rgb(255, 50, 50); border-image: none; text-align: center; color: rgb(255, 255, 255); line-height: 20px; font-size: 14px; font-weight: bold; white-space: nowrap; position: relative; cursor: pointer; float: right; box-shadow: none; background-color: rgb(255, 50, 50);
}

.tweetscontainer{
 background: rgb(255, 255, 255); list-style: none; margin: 0px; padding: 0px;
}

.tweetcontainer{
 margin: 0px; padding: 10px; overflow: hidden; border-bottom-color: rgb(230, 236, 240); border-bottom-width: 1px; border-bottom-style: solid;
}

.tweetprofimg{
 border-radius: 50%; width: 48px; height: 48px; margin-right: 10px; float: left;
}

.tweetprofname{
 color:#14171a;font-size:14px;font-weight:bold;
}

.tweetprofuid{
 color:#657786;font-size:14px;margin-left:5px;
}

.tweetstats{
 margin-right: 30px; cursor: pointer;
}

.tweetstats i{
 color: rgb(101, 119, 134); font-size: 16px;
}

.tweetstatscount{
 color: rgb(101, 119, 134); line-height: 1; font-size: 12px; font-weight: bold; margin-left: 6px;
}

.red i, .red span{
 color:#f00 !important;
}

Now let’s check how our files are looking in the browser. Right click on the `login.html` and select Open With > Web Browser, as shown in the image below:

creating-twitter-clone-ss-6

The login/registration screen will look like the image below:

creating-twitter-clone-ss-7

Similarly, when you open the `profile.html` file, it will look like this:

creating-twitter-clone-ss-8

Right now nothing will happen if you click on the tweet button on profile page. Even letters counter will not function when you enter text in the box. We have used the picture of Camilla Belle, but if you wish you can change the images in the code.

Let’s make our application functional by adding JavaScript in it. First of all, create a JavaScript file for login screen. We will call it `login.js`. To create a JavaScript file, right click on `twitterclone` directory in Project Explorer and select New > JavaScript Source File.

creating-twitter-clone-ss-9

On the next screen you will need to enter the name of the JavaScript file, for example, `login.js`.

creating-twitter-clone-ss-10

The new JavaScript file will look like this:

creating-twitter-clone-ss-11

`login.js` file will hold the values of all the text fields in the login/registration screen. It will check whether the values are of appropriate type and not empty. Also, it will set the cookies with user ID and name, so that we can later use them in our profile/wall page. Below is the code of `login.js` file.

$(function(){
     $name = $('#full-name');
     $nameerror = $('#nameerror');
 
     $uid = $('#uid');
     $uiderror = $('#uiderror');
 
     $email = $('#email');
     $emailerror = $('#emailerror');
 
     $pass = $('#password');
     $passerror = $('#passerror');
 
     $('#submit_button').click(function(){
 
     $error = 0;
     $nameerror.hide();
     $uiderror.hide();
     $emailerror.hide();
 
     if(($nameval = $.trim($name.val())).length == 0)
     {
         $error = 1;
         $nameerror.show();
     }
 
     if(($uidval = $.trim($uid.val()).length == 0))
     {
         $error = 1;
         $uiderror.show();
     }
     else
     {
         $uidval = $uidval.replace(/\s+/g, '');
     }
 
     if(($emailval = $.trim($email.val())).length == 0)
     {
         $error = 1;
         $emailerror.show();
     }
     else if(!(/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test($emailval)))
     {
         $error = 1;
         $emailerror.show();
     }
 
     if(($passval = $.trim($pass.val())).length == 0)
     {
         $error = 1;
         $passerror.show();
     }
 
     if(!$error)
     {
         Cookies.set('name', $nameval);
         Cookies.set('uid', $uidval);
 
         location.href = 'profile.html';
     }
 });
});

If there are no errors, then two cookies will be set – name and uid. Also, the current page will get redirected to `profile.html`.

JavaScript Made Easy With Webclipse

There are so many wonderful features that Webclipse provides for the development of JavaScript application! Let’s see each one of them.

Syntax Highlighting

Webclipse provides a rich set of colors with proper contrast for both the dark and light themes of Eclipse, so that our code syntax can be clear and readable. This helps developers to understand parts of code in a jiffy.

webclipse-syntax-highlighting

Content Assist

Webclipse can help you in predicting the right functions to be called in particular situations. This will make your work fast and error free. If you forget the name of some function, then content assist will provide you with the list of all the functions from which you can easily recall the one you need. Look at the images below to see it in action.

webclipse-content-assist

In the above image, content assist is showing that there are only two variables which contain the word ’email’. We can use either of them, in accordance with our requirements.

webclipse-content-assist-functions

In this image, content assist is showing that we can use any of the listed functions over the `$emailerror` variable. `$emailerror` holds the reference of jQuery selector for showing errors in the email field on the login page.

Instant Validation

How often do we skip a semi-colon at the end of a line, or do we sometimes put a semi-colon at the end of a function? Yes, sometimes we all do, while occasionally we commit other types of syntax errors. With Webclipse you will get the realtime validation so that you won’t have to bear the ache of unexpected behaviors during code execution. The image below shows the instant validation functionality of Webclipse.

webclipse-instant-validation

Here we forgot a parenthesis at the end of `if` statement, and Webclipse instantly informed us about the error.

webclipse-instant-validation-error-description

Webclipse gives a brief description of where and what the error is.

There are a number of other benefits of Webclipse that we can enjoy when dealing with JavaScript or jQuery. Some of them include the following:

  1. Integrated Debugging
  2. Call and Type Hierarchies
  3. Source Refactoring
  4. Source Formatting
  5. Occurrence Highlighting

We will look at all these features in our future articles. Let’s now create a JavaScript file for profile/wall page. Like before, right click on `twitterclone` directory in project explorer window and select New – JavaScript Source File. We will call this file – `profile.js`.

There are few functions of `profile.js` file:

  1. To set the name and userid of the user which was provided in the login page and stored in name and uid cookies by login.js file.
  2. To increase the count of tweets on the left profile card as soon as user posts a tweet.
  3. Keeping track of tweets character counts.
  4. Posting a tweet at the press of tweet button.
  5. Retweet functionality.
  6. Like functionality.

Here is the code of `profile.js` file:

$(function(){
 
     $('#profilecardname').text(Cookies.get('name'));
     $('#profilecarduid').text('@'+Cookies.get('uid'));
 
     $statslistitemcount = $('#statslistitemcount');
 
     $totalchars = $('#totalchars');
 
     $posttweetta = $('#posttweetta');
 
     $tweetscontainer = $('#tweetscontainer');
 
     $posttweetta.keypress(function(e){
     if (e.keyCode == 13 && !e.shiftKey)
     {
         e.preventDefault();
         return false;
     }
 
 });
 
 $posttweetta.keyup(function(e){
     $totchars = $(this).val().length;
         if($totchars <= 250)
         $totalchars.text($totchars);
     else
     {
         $totalchars.text('250');
         $(this).val($(this).val().substring(0, 250));
     }
 });
 
 $('#posttweetbut').click(function(){
     if(($taval = $.trim($posttweetta.val())).length > 0)
     {
         $tweetscontainer.prepend(tweetitem($taval));
         $posttweetta.val(''); 
         $statslistitemcount.text(parseInt($statslistitemcount.text()) + 1); 
     }
 });
 
 $tweetscontainer.on('click', 'span.retweet', function(){
     $tweetstatscount = $(this).children('.tweetstatscount');
     $tweetstatscount.text(parseInt($tweetstatscount.text()) + 1);
 
     $tweetscontainer.prepend(tweetitem($tweetstatscount.closest('.tweetcontainer').find('p').text()));
     $statslistitemcount.text(parseInt($statslistitemcount.text()) + 1); 
 });
 
 $tweetscontainer.on('click', 'span.like', function(){
     $tweetstatscount = $(this).children('.tweetstatscount');
     if($(this).hasClass('red'))
     {
         $(this).removeClass('red');
         $tweetstatscount.text(parseInt($tweetstatscount.text()) - 1);
     }
     else
     {
         $(this).addClass('red');
         $tweetstatscount.text(parseInt($tweetstatscount.text()) + 1);
     }
 });
 
 function tweetitem($taval)
 {
     return '<li class="tweetcontainer">'+
                 '<img class="tweetprofimg" src="http://3.bp.blogspot.com/-JCYefwq__2U/TxCfC3s1ZpI/AAAAAAAAKcM/u5mw7qPAL0w/s300-c/Camilla-Belle-6.jpg">'+
                 '<span class="tweetprofname">'+Cookies.get('name')+'</span>'+
                 '<span class="tweetprofuid">@'+Cookies.get('uid')+'</span>'+
                 '<div class="ml58px">'+
                     '<p style="margin: 0px;">'+$taval+'</p>'+
                     '<div class="mt10px">'+
                         '<span class="retweet tweetstats">'+
                             '<i class="fa fa-retweet"></i>'+
                             '<span class="tweetstatscount">0</span>'+
                         '</span>'+
                         '<span class="like tweetstats">'+
                             '<i class="fa fa-heart-o"></i>'+
                             '<span class="tweetstatscount">0</span>'+
                         '</span>'+
                     '</div>'+
                 '</div>'+
             '</li>';
 }
});

twitter-clone-profile-js-file

There is one more thing we can add to this project. Suppose somebody directly opened the `profile.js` file without logging in – what will happen? There will be no set cookies for their name and user ID. This will provide null and undefined values. Look at the image below:

twitter-clone-profile-js-file-undefined-values

Here we are not getting any name on the card, and userid field is undefined.

To solve this problem, we can explicitly check whether the cookies for name and user ID are set or not. If they are not set, then we can redirect the profile page to the login page. The code for this task is written below:

 <script>
     if(Cookies.get('name') == undefined || Cookies.get('uid') == undefined)
         location.href = 'login.html';
 </script>

We need to insert this code in the head section of `profile.htm` file and after including the `Cookie.js` external script. Look at the image below:

twitter-clone-profile-js-file-cookies-included

Our project is complete and fully functional now. To test this in a browser, right-click the `login.html` file and choose Open With > System Editor to open the app in your system’s browser.

Note: Since we are opening the html file directly in a browser using the `file://` protocol, the app will work with browsers Firefox and Edge, which allow cookies to work with locally served files. However, it will not work with Chrome for instance, which does not allow cookies to be used in files served with the `file://` protocol, and will require that the files be served with a web server.