Making a Twitter Clone in Raw HTML with Ruby on Rails and a Lil JavaScript

Ethan Ryan
codeburst
Published in
14 min readMay 11, 2017

--

The internet is a place.

The internet is lots of things, like a series of tubes, and a global system of interconnected computer networks, and a means of looking at cats.

But it’s also a place.

It’s been called the eighth continent. It’s also been called the World Wide Web and the Information Superhighway. The world is a big, wide place. A cobweb is a small place built by spiders to catch prey. Sticky strings of silk connecting nodes within a latticed grid where flies twitch and await their doom. A highway connects places to each other, but a highway is a place too. It’s a big road where you sometimes get stuck in traffic.

space is the place

The internet is a big, colorful library where everyone is screaming.

The internet has routes and sites and addresses. It has firewalls and paywalls and windows. It has backdoors and wormholes and hyperlinks.

It’s a little bit like a video game, or an arcade. There are buttons everywhere.

Cyberspace is a lot like meatspace. There are good places and bad places, popular places and lame places, interesting places and dead ends.

Social media sites are popular places where people hangout and share stories and stare at each other and get harassed and bullied. I prefer Wikipedia.

Each social media site has its own vibe.

LinkedIn is the beige waiting room of an employment office where everyone is sitting around staring at their resumes. There are a couple of executives, but mostly it’s middle managers and administrative assistants. The mood is both desperate and boring. Everyone there wants to be somewhere else.

Facebook is a high school reunion, but it’s 24/7, and instead of your high school gymnasium it’s at some rich nerd’s house, and your friend’s mom is there for reason, and she keeps talking about some article she read on the New York Times.

Instagram is a trendy brunch place where everyone is taking pictures of their food, but no one’s actually eating the food, because later they’re going to the beach to take pictures of their navels.

Snapchat is a self-destructing selfie photo booth made by some guy who wanted a way to send and receive naked pictures on his phone.

Twitter is a street corner soapbox for weirdos to express themselves in fortune cookie-size bursts of text. People on Twitter talk to each other but mostly just talk to themselves. Twitter is my favorite.

So to practice my coding skills, I made a simple, skeletal version of Twitter.

Here are my step-by-step instructions, for anyone wanting to make something similar:

Steps:

1) domain modeling

HTMLtwitter domain model

User stories:

  • As a visitor to the site, I should be able to sign up for the app as a new user, or log in if I already have an account.
  • Users must have passwords and unique usernames to sign in.
  • As a logged in user, I should be able to see all the users, and all their tweets.
  • As a user, I should be able to follow (and unfollow) other users.
  • As a user, I should be able to see my profile page with all my tweets.
  • As a user, I should be able to create and post my own tweets, and delete my own tweets.
  • As a user, I should be able to edit my username, bio, and profile picture
  • As a user, I should be able to ‘like’ tweets (my own and other people’s)

For now, no retweets or tweeting at other users, just following, tweeting, and liking tweets.

I got pretty confused about the relationship part, and after some googling decided this way will work. A relationship belongs to users, and to followers, who are users. So our user model will also keep track of follower_id, and our relationship model will keep track of both user_id and follower_id. hopefully that’ll work, but we’ll tweak it if not.

2) create repo on GitHub

  • create a local folder on your computer
  • clone your repo into your local folder
  • cd into your master branch

3) create rails project

In terminal:

rails new html_twitter_app

4) make your first pull request

why not? it’s a good idea to save early, save often.

git add .

git commit -m “first commit to html_twitter_app project”

git push

5) create your models and migration files

Based on our modeling above, we’ll be creating 4 models for this project.

In terminal:

rails generate model User username password_digest bio profile_pic follower_id:integer profile_pic:binary

Note 1: string is the default data type, so for every table column that isn’t a string, we need to list what kind of data type it will be.

Note 2: not sure about profile pics, but some googling said the data type to use for images is binary. we’ll figure that out later, for now it’s good to having something on the table.

rails generate model Tweet user_id:integer content

Note 3: the rails model generator should add timestamp fields automatically for all our database tables, so our tweets should include the time they were created_at.

rails generate model Relationship user_id:integer follower_id:integer

rails generate model Like tweet_id:integer user_id:integer

Cool! Now we’ve got 4 models in our app/models directory, and 4 migration files in our db/migrate folder.

Check out your migration files to make sure your table columns and their corresponding data types are looking good.

6) run migrations to create your tables in your database

In terminal:

rake db:migrate

This will run your migrations and create your tables in ActiveRecord database.

This will also create a schema.rb file in your db folder.

7) set up associations between models

A model that belongs to another model has its owner’s foreign keys on its table, and the class objects also need to reflect those relationships.

So in models/tweet.rb:

class Tweet < ApplicationRecord
belongs_to :user
end

And in models/user.rb:

class User < ApplicationRecord
has_many :tweets
end

Likewise, in models/like.rb:

class Like < ApplicationRecord
belongs_to :user
belongs_to :tweet
end

And in models/user.rb:

class User < ApplicationRecord
has_many :tweets
has_many :followers, :class_name => “Relationship”, :foreign_key => “user_id”
has_many :following, :class_name => “Relationship”, :foreign_key => “follower_id”
end

And in models/relationship.rb:

class Relationship < ApplicationRecord
belongs_to :user
belongs_to :follower, class_name: “User”
end

(Note: wtf for followers and following stuff? I found that here:

http://stackoverflow.com/questions/3397920/relationship-like-twitter-followers-followed-in-activerecord

Changed the model / class_name from “Followings” to “Relationship”. hopefully that’s gonna work!)

Now that we’ve established these relationships btwn our models or object classes, we can test them out to see that their working in the console.

In the terminal:

rails c

This brings up our rails console.

then:

User.all

This shows us all our users. so far, we don’t have any. so let’s make some.

User.create({username: “bob”})

User.create({username: “susan”})

Now when we run User.all, we can see our two users, with IDs #1 and #2, and the usernames bob and susan.

Cool, our database now has some data in it.

8) make a page to see all the users

Time to start seeing our app in action in the browser. this is where all our hard work starts paying off.

Let’s first see a list of all our users.

This will fulfill half of one of our user story requirements.

Whenever we make changes to our app, we should ask ourselves two questions:
1) does this change our schema? if so, we’ll need to update our database tables.
2) what url will take us there? we want to be RESTful, and mindful of our routes.

For the above page, we won’t need to change our schema, and the url we’ll want to use to see all our users will be an HTTP GET request.

Reminder/review:

CRUD
create — POST
read — GET
update — PATCH (or PUT)
destroy — DELETE

get ‘/users’ will show all our users.

Cool. Let’s take a look at our app in the browser.

In terminal:

rails s

will run our local server.

Listening on tcp://localhost:3000

Visit localhost:3000 and you should see:

Yay! You’re on Rails!

Now visit: localhost:3000/users

The error tells us “No route matches [GET] “/users”

Errors are great. They tell us what we need to do to get our app operating the way we want to it to look and behave. That’s what error driven development is all about: letting the errors drive our development process.

To fix the above error, we need to go into config/routes.rb and add that route.

There are two ways to fix the above error in this file:

get ‘/users’, to ‘users#index’

resources :users, only: [:index]

We’ll go with the second.

Now we can also run in the terminal:

rake routes

To see all our routes.

Cool, we can see the route we just created.

Now let’s check out in the browser:

http://localhost:3000/users

We have a new error:

uninitialized constant UsersController

Cool, let’s fix that by going into our controllers folder and making a controller for Users.

For now we just need an index action.

So after creating and updating our controllers/users_controller.rb file, it’ll look like this:

class class UsersController < ApplicationControllerdef index
@users = User.all
end
end

now when we visit http://localhost:3000/users, we should see:

UsersController#index is missing a template for this request format and variant.

missing a template means we’re missing a View page. so let’s make a View page in:

app/views/users/index.html.erb

and for now, let’s just give that page a nice greeting:

<h1>hello world</h1>

now when we visit http://localhost:3000/users, we should see:

hello world

in big black basic HTML. sweet! progress!

ok, back to our mission: let’s see all the users.

let’s add some embedded ruby to our index.html.erb page:

<h1>All htmlTwitter Users</h1><% @users.each do |user| %>
<p><%= user.username %></p>
<% end %>

now when we visit http://localhost:3000/users, we should see:

All htmlTwitter Users

bob

susan

Awesome! The client sent a request, the request hit our router, the router sent the request to the controller action, the controller action composed a template by looking at the view file, and sent back a response to the client, which rendered the response.

This process will be repeated for every success page visit for our app.

In this particular example, the browser made a GET request to our app’s database, then printed out our app’s usernames to the browser in glorious HTML! We’re doing great.

(Note: we could use css or a css framework like Materialize or Bootstrap to make our app look nice, but my goal with this project is to have a minimalist, brutalist, skeletal html app, so that’s what I’m gonna do. No styling. No fancy fonts or colors. Keeping it simple for this project, just basic black and white.)

9) also show all the user’s tweets

Let’s update our page to also show all the tweets of each user:

<h1>All htmlTwitter Users</h1><% @users.each do |user| %>
<p><%= user.username %></p>
<%= user.tweets.each do |tweet| %>
<! — above is adding all the tweets for each user under their username →
<p><%= tweet.content %></p>
<% end %>
<% end %>

Now when we visit the page, it displays:

1) all users

Oh yeah, our users haven’t tweeted anything yet. We’ll get there soon. But when they do have tweets, this page will now display them.

10) create profile page for each user, which shows all their tweets

What will our route be?

user/id

So we’ll update our routes:

resources :users, only: [:index, :show]

Now we have a show page for users.

Let’s also update our User controller, and add a show controller.

def show
@user = User.find(params[:id])
end

Now let’s make our users/show.html.erb page:

<h1>Profile Page for <%= @user.username %></h1><p><%= @user.username %></p><% @user.tweets.each do |tweet| %>
<! — above is adding all the tweets for each user under their username →
<p><%= tweet.content %></p>
<% end %>

if we visit http://localhost:3000/users/1, we’ll see:

Profile Page for bob

bob

Now let’s make it so we can link to each user’s show page from our user index page.

In user/index.html/erb:

<h1>All htmlTwitter Users</h1><ul>
<% @users.each do |user| %>
<li><%= link_to user.username, user_path(user) %></li>
<% user.tweets.each do |tweet| %>
<! — above is adding all the tweets for each user under their username →
<p><%= tweet.content %></p>
<% end %>
<% end %>
</ul>

We’ve added an unordered list, and made each username a list item within it.

Now each username is a link, that when clicked will take us to their profile page.

Nice.

11) new tweets form

This is a twitter app, let’s start making some tweets!

We’ve already established our relationships, and run our migrations, so the schema doesn’t need to change.

What URL should take the user to a page to create tweets?

‘tweets/new’ makes sense.

We’ll create that page, and put a form on it.

Making a POST request to ‘/tweets’ should create the new tweet.

In our routes, we’ll add:

resources :tweets, only: [:new, :create]

Let’s also create a tweets_controller, with the actions for new and create.

def new
@tweet = Tweet.new
end
def create
@tweet = Tweet.new
end

Then we’ll add some show page for tweets.

And put a form_for on it.

<h1>New Tweet Form</h1><%= form_for(@tweet) do |f| %>
<%= f.label :content %>
<%= f.text_field :content %>
<br><br>
<%= f.label :user_id %>
<%= f.text_field :user_id %>
<br><br>
<%= f.submit %>
<% end %>

We’ll be refactoring this form later, but for now it’s a way for us to create some pretty basic tweets, with content and a user_id.

For our tweet controller, we’ll add:

def create
tweet = Tweet.new(tweet_params)
if tweet.save
redirect_to user_path(tweet.user)
else
render ‘new’
end
end
privatedef tweet_params
params.require(:tweet).permit(:user_id, :content)
end

Here’s our new tweet form page:

2) new tweet form page

And after hitting submit, that form redirects us to:

3) redirect to profile page

Awesome! We’ve create our first tweet. Our new tweet form associates a tweet to a user based on the user_id, and redirects to that user’s profile page after the form is submitted. The profile page will show all the user’s tweets. let’s do another to see how it looks.

4) tweet #2

Looking good. Let’s give susan some tweets too while we’re at it.

5) susan’s tweets

We’ve now got two users, and each have two tweets to their name. Wow. We’re on fire.

12) tweet show page

Let’s give each tweet a show page too.

Routes:

resources :tweets, only: [:new, :create, :show]

This will be our route, for now:

http://localhost:3000/tweets/1

And this will be our tweet/show page:

<h1>Profile Page for <%= @tweet.id %></h1>

<p><%= @tweet.content %></p>

And since the user has many tweets, we’ll make each tweet clickable from the users’s profile page, so you can click on each tweet and see its show page via the user’s index page.

We’ll include a link back to the tweet’s user as well.

13) user sign in form

So far, we have two users. we want them to be able to sign in via a sign in form.

We’ll need some sessions. but we won’t need to add a sessions table to our database.

Let’s add our routes:

get ‘/login’, to: ‘sessions#new’, as: ‘login’
post ‘/sessions’, to: ‘sessions#create’, as: ‘sessions’

Let’s add a sessions controller, with actions new and create.

Now create a View page template for sessions/new.html.erb.

So when we visit: http://localhost:3000/login

It should show us:

login

Cool. Let’s add a form_tag to the page.

<h1>Login Page</h1>

<%= form_tag :sessions do %>
<%= label_tag :username %>
<%= text_field_tag :username %>
<%= label_tag :password %>
<%= password_field_tag :password %>
<%= submit_tag “Log In” %>
<% end %>

Now we have a form for people to log in with.

13) tweet time and dates

Let’s add to our tweets:

Tweet.first.created_at

14) twitter home page = twitter index page

Instead of all the users, we want all the tweets as our main page.

I added the tweets dates on that, and made it so we can click on each user name to view their userpage.

Eventually, this page will only show the tweets of whomever the user follows.

Here’s our code for our twitter index page:

<ul>
<% @tweets.each do |tweet| %>
<strong><%= link_to tweet.user.username, user_path(tweet.user_id) %></strong>
<%= tweet.created_at.strftime(“%d %b %Y at %I:%M %p”) %>
<p><%= tweet.content %></p>
— — — — — — — — — — — — — — — — — — — — — — — — — — — -
<br><br>
<% end %>
</ul>

6) all tweets index page

15) sort tweets in descending order

Let’s add the current user and a link to create tweets from our tweets/index page:

<h3>Current User: <%= @tweets.first.user.username %></h3>
<h3>Create New Tweet: <%= link_to “New Tweet”, new_tweet_path(@tweets) %></h3>

And let’s create a new tweet, saying “this should be on top!”

Now we need to sort our tweets, so they are in descending order, instead of ascending order like they are now:

7) homepage, before sorting by most recent first

Ruby is a friendly programming language, with lots of built in methods.

In this case, all we need to do is add .reverse to our iteration through all the tweets:

<% @tweets.reverse.each do |tweet| %>

8) homepage, most recent tweet first

(pic of twitter/index, order descending after adding .reverse to each)

We’ll also add .reverse on our user show page, so each user’s tweets are show in descending order.

15555555) require login

Since people can login, let’s make it so they have to login.

At the top of all our controller pages, we’ll add the following:

before_action :require_login

As the above line implies, this requires login for any user trying to access any actions.

We don’t want to exclude users from logging in, so we’ll make an exception for ourUsersController:

before_action :require_login, except: [:new, :create]

This way, users who aren’t logged in can see the login form and can log in, but they can’t do anything else unless they’re logged in.

But we need to add some authentication, so we know people are using the correct password!

For password authentication, uncomment in your gem file:

gem ‘bcrypt’, ‘~> 3.1.7’

and run bundle install.

In the User class, include:

has_secure_password

22???) realized i needed to add another column to my Like table: “count”

rails generate migration add_count_to_likes count:integer

Then:

rake db:migrate

UPDATE: this was so stupid. i could just call the built in Ruby method, .count, on the like instance.

99) I added some a placeholder profile pic, a nerd gif

Now our users have a face.

It’s the same face, but one face is better than none.

100) some Javascript to brighten up the like button on click

Finally, added a fun Javascript effect for whenever someone likes a tweet.

Still no CSS, this is all Javascript.

9) finally, some color

This project made me appreciate how much effort goes into making a dynamic website. So much information is contained in a single tweet! We’re lucky to have so many fun sites to play with. Cyberspace is the place.

--

--