Skip to content

Instantly share code, notes, and snippets.

@jovertical
Last active May 2, 2020 14:30
Show Gist options
  • Save jovertical/077ef980bcfe0f80b4465e31d2f2fe34 to your computer and use it in GitHub Desktop.
Save jovertical/077ef980bcfe0f80b4465e31d2f2fe34 to your computer and use it in GitHub Desktop.
Tweety tutorial

1. Project Overview

This tiny twitter clone project must consist the following features:

  1. User registration & login
  2. Profile
  3. Follow other users
  4. Tweet
  5. Like each others tweet

2. Install Node.js & NPM

To get started, we must ensure that we have Node.js as for NPM this usually is packaged along with Node.js, once the installation is complete, verify in your terminal:

node -v

npm -v

3. The Next.js Installer

We will use Next.js as our React.js framework as it have batteries included out of the box. To get started, we can use the create-next-app CLI tool:

npm i create-next-app -g

4. Create Tweety using the CLI tool

Creating a Next.js app from the CLI includes a basic boilerplate with to save us some time:

create-next-app tweety

5. Supercharge your IDE

In this tutorial, VSCode is our text editor of choice so having that installed in your system is necessary.

Using recommended settings

I already setup an appropriate one, we just need to copy the settings.json file inside the .vscode directory:

mkdir .vscode && touch .vscode/settings.json

Make sure to copy and paste the following:

{
  "emmet.includeLanguages": {
    "javascript": "javascriptreact"
  },
  "css.validate": false,
  "editor.formatOnSave": true,
  "editor.tabSize": 2,
  "editor.insertSpaces": true,
  "editor.detectIndentation": false
}

Installing recommended extensions

Again, I already setup an appropriate one, we just need to copy the extensions.json file inside the .vscode directory:

touch .vscode/extensions.json

Make sure to copy and paste the following:

{
  "recommendations": [
    "redhat.vscode-xml",
    "bradlc.vscode-tailwindcss",
    "dbaeumer.vscode-eslint",
    "hex-ci.stylelint-plus",
    "ofhumanbondage.react-proptypes-intellisense",
    "dsznajder.es7-react-js-snippets",
    "visualstudioexptteam.vscodeintellicode"
  ]
}

💡 Then, we must restart VSCode, and when a "This workspace has extension recommendations." message pops up, just click "Install All"

6. Install React developer tools

For chrome users: https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en

For firefox users: https://addons.mozilla.org/en-US/firefox/addon/react-devtools/

7. Install MySQL

Install the MySQL server: https://dev.mysql.com/downloads/mysql/

Learn more

8. Install Table plus (Database GUI)

https://tableplus.com/

1. Basic routing & pages

Next.js has a file-system based router so when a file typically a React component is placed inside the pages directory, it will be automatically available as a route, learn more.

The Home page

It is typically the index.js file. Since we used the create-next-app CLI, its already there for us but be sure to update its content:

import React from 'react'

export default function Home() {
  return <h1>Home page</h1>
}

The Profile page

This page must be guarded, meaning a user must be authenticated when accessing this page, but for the mean time we can just create a profile.js file inside pages and then the page component:

import React from 'react'

export default function Profile() {
  return <h1>Profile page</h1>
}

2. Linking between pages

Of course, we can just visit / or /profile, create an anchor tag: <a href="/profile">Profile</a> thats a given but it might not make sense all the time as we will be missing out on the capabilities of a Single Page App (SPA).

Link home page & profile

Next.js has a Link component we can utilize, lets go ahead and use it:

Home page

View Code Snippet

import React from 'react'
import Link from 'next/link'

export default function Home() {
  return (
    <div>
      <nav>
        <div>
          <Link href="/">
            <a>Home</a>
          </Link>
        </div>
        <div>
          <Link href="/profile">
            <a>My Profile</a>
          </Link>
        </div>
      </nav>

      <main>
        <h1>Hello tweety</h1>
      </main>
    </div>
  )
}

Profile page

View Code Snippet

import React from 'react'
import Link from 'next/link'

export default function Profile() {
  return (
    <div>
      <nav>
        <div>
          <Link href="/">
            <a>Home</a>
          </Link>
        </div>
        <div>
          <Link href="/profile">
            <a>My Profile</a>
          </Link>
        </div>
      </nav>

      <main>
        <h1>Profile page</h1>
      </main>
    </div>
  )
}

3. Difference between server & browser rendering

When we visit a page from the address bar: http://localhost:3000/profile, it is server rendered. In fact if we define a getInitialProps in the page:

...

Profile.getInitialProps = async ctx => {
  console.log('Im in the server!')
  return { followers: [] }
}

💡 Reload the page and as we can see, it will print out only in the terminal, not in the browser console.

1. Database setup

We will use MySQL in development and in production (if we got to deploy in Vercel)

Create a blank database

mysql -uroot -p

mysql> CREATE DATABASE tweety;

2. Hello Knex

Before anything else, let's install Knex and Node.js' mysql database driver:

npm i knex mysql

Once complete, create a knexfile.js in the project root and copy and paste the following:

View Code Snippet

const baseSettings = {
  migrations: {
    directory: './database/migrations'
  },
  seeds: {
    directory: './database/seeds'
  }
}

module.exports = {
  development: {
    client: 'mysql',
    connection: 'mysql://root:@localhost/tweety',
    ...baseSettings
  },
};

3. Migrations

To start, we must install Knex globally:

npm i knex -g

Then, we need to create the following migrations:

  1. users - id, username, name, avatar, email, password, created_at, updated_at
  2. tweets - id, user_id, body, created_at, updated_at

Now lets create the users migration:

knex migrate:make users

Then copy and paste this to the users migration file we just created:

View Code Snippet

exports.up = function (knex) {
  return knex.schema.createTable('users', table => {
    table.increments('id')
    table.string('username').notNullable()
    table.string('name')
    table.string('avatar')
    table.string('email').notNullable()
    table.string('password')
    table.timestamps()

    table.unique(['username', 'email'])
  })
};

exports.down = function (knex) {
  return knex.schema.dropTable('users')
};

Then copy and paste this to the tweets migration file we just created:

View Code Snippet

exports.up = function (knex) {
  return knex.schema
    .dropTableIfExists('tweets')
    .createTable('tweets', table => {
      table.increments('id')
      table.integer('user_id').unsigned()
      table.string('body')
      table.timestamps()

      table.foreign('user_id').references('users.id')
    })
};

exports.down = function (knex) {
  return knex.schema.dropTable('tweets')
};

Once the migrations are created, we can go ahead and run:

knex migrate:up

4. Models

Knex.js is just a query builder, so we need to have something that can represent the entities / models.

Now let's install Objection.js:

npm i objection

Then, create an app/models directory and create a file named User.js, copy and paste:

View Code Snippet

const { Model } = require('objection');
const Tweet = require('./Tweet');

class User extends Model {
  static get tableName() {
    return 'users';
  }

  static get relationMappings() {
    return {
      tweets: {
        relation: Model.HasManyRelation,
        modelClass: Tweet,
        join: {
          from: 'users.id',
          to: 'tweets.user_id'
        }
      }
    }
  }
}

module.exports = User;

Finally, create the Tweet.js file, copy and paste:

View Code Snippet

const { Model } = require('objection');
const User = require('./User');

class Tweet extends Model {
  static get tableName() {
    return 'tweets';
  }

  static get relationMappings() {
    return {
      user: {
        relation: Model.BelongsToOneRelation,
        modelClass: User,
        join: {
          from: 'tweets.user_id',
          to: 'users.id'
        }
      }
    };
  }
}

module.exports = Tweet;

5. Fetching Tweets

The thing is, we need to fetch tweets from the users who it follows, but for now let's just fetch all the tweets:

View Code Snippet

...

export async function getServerSideProps() {
  // TODO: Extract this to a separate file or create a custom App entrypoint.
  const { Model } = require('objection')
  const db = knex(require('../knexfile').development)
  Model.knex(db)

  const Tweet = require('../app/models/Tweet')
  const tweets = await Tweet.query()
    .withGraphFetched('user')
    .select('id', 'body')

  return {
    props: {
      tweets: JSON.parse(JSON.stringify(tweets))
    }
  }
}

1. Render a list of tweets

Since we are passing a tweets prop from getServerSideProps(), we can put that to use:

export default function Home({ tweets }) {
...

      <main>
          <h1>Recent tweets</h1>

          <div>
            {tweets.map(tweet => (
              <div key={tweet.id}>
                <h4>{tweet.user.name}</h4>
                <p>{tweet.body}</p>
              </div>
            ))}
          </div>
      </main>

...

2. Hello Tailwind CSS

Of course, we need a CSS framework, we will be using Tailwind CSS for this project.

Now let's install it:

npm install tailwindcss postcss-preset-env --dev

Then, create a styles/app.css file and put:

@tailwind base;
@tailwind components;
@tailwind utilities;

Then, generate the config file (optional):

npx tailwindcss init

Then, Create a postcss.config.js file, put:

module.exports = {
  plugins: ['tailwindcss', 'postcss-preset-env'],
}

Finally, create a pages/_app.js & load the app.css file:

import '../styles/app.css'

export default function Tweety({ Component, pageProps }) {
  return <Component {...pageProps} />
}

💡 Restart, and apply some styles!

3. Structure the Layout

First step is to create a wrapping div, with 3 containers inside that would be the sidebar, main content & right side widget, now pages/index.js would look like this:

View Code Snippet

export default function Home(props) {
  return (
    <div className="h-screen flex justify-between overflow-hidden">
      <div className="w-96 bg-gray-dark border-r border-gray-700">
        <div className="w-2/3 ml-auto pr-16">
          <div className="py-4">
            <svg className="w-8 h-8 text-white" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30.782 28.34">
              <g transform="matrix(0.951, -0.309, 0.309, 0.951, -1.778, 6.973)">
                <path d="M220.961,16.113,230.8,21.3V0l-9.841,5.185Zm0,0" transform="translate(-208.241)" />
                <path d="M451.953,169.988h3.457v1.727h-3.457Zm0,0" transform="translate(-425.935 -160.203)" />
                <path d="M434.494,73.172l.661,1.6-3.194,1.323-.661-1.6Zm0,0" transform="translate(-406.471 -68.96)" />
                <path d="M434.483,249.024l-3.194-1.323.661-1.6,3.194,1.323Zm0,0" transform="translate(-406.461 -231.939)" />
                <path d="M10.993,99.934H3.457v1.728H3.168a3.168,3.168,0,1,0,0,6.336h.289v1.729H5.391l1.9,6.921h5.6l-1.9-6.638Zm0,0" transform="translate(0 -94.181)" />
              </g>
            </svg>
          </div>
          <nav className="mt-2">
            <Link href="/">
              <a className="flex text-white py-3">
                <svg className="w-6 h-6" fill="none" stroke="currentColor" strokeWidth="1.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.5 21.5">
                  <g transform="translate(-2.25 -1.25)">
                    <path d="M3,9l9-7,9,7V20a2,2,0,0,1-2,2H5a2,2,0,0,1-2-2Z" />
                    <path d="M9,22V12h6V22" />
                  </g>
                </svg>
                <span className="ml-4">
                  Home
                </span>
              </a>
            </Link>
            <Link href="/profile">
              <a className="flex text-white py-3">
                <svg className="w-6 h-6" fill="none" stroke="currentColor" strokeWidth="1.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17.5 19.5">
                  <g transform="translate(-3.25 -2.25)">
                    <path d="M20,21V19a4,4,0,0,0-4-4H8a4,4,0,0,0-4,4v2" />
                    <circle cx="4" cy="4" r="4" transform="translate(8 3)" />
                  </g>
                </svg>
                <span className="ml-4">
                  My Profile
                </span>
              </a>
            </Link>
            <div className="py-3">
              <button className="bg-blue w-full rounded-full text-white py-3">
                Start Ranting
              </button>
            </div>
          </nav>
        </div>
      </div>

      <main className="flex-1 bg-gray-dark">
        <div className="border-b border-gray-700">
          <h1 className="text-xl text-white font-bold px-8 py-4">Home</h1>
        </div>
      </main>

      <div className="w-96 bg-gray-dark border-l border-gray-700"></div>
    </div>
  )
}

Now, the sidebar is in the homepage, but how about the other pages? tldr; lets extract those and create a components/layout.js file and put:

View Code Snippet

import React from 'react'
import Link from 'next/link'

export default function Layout({ title, children }) {
  return (
    <div className="h-screen flex justify-between overflow-hidden">
      <div className="w-96 bg-gray-dark border-r border-gray-700">
        <div className="w-2/3 ml-auto pr-16">
          <div className="py-4">
            <svg className="w-8 h-8 text-white" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30.782 28.34">
              <g transform="matrix(0.951, -0.309, 0.309, 0.951, -1.778, 6.973)">
                <path d="M220.961,16.113,230.8,21.3V0l-9.841,5.185Zm0,0" transform="translate(-208.241)" />
                <path d="M451.953,169.988h3.457v1.727h-3.457Zm0,0" transform="translate(-425.935 -160.203)" />
                <path d="M434.494,73.172l.661,1.6-3.194,1.323-.661-1.6Zm0,0" transform="translate(-406.471 -68.96)" />
                <path d="M434.483,249.024l-3.194-1.323.661-1.6,3.194,1.323Zm0,0" transform="translate(-406.461 -231.939)" />
                <path d="M10.993,99.934H3.457v1.728H3.168a3.168,3.168,0,1,0,0,6.336h.289v1.729H5.391l1.9,6.921h5.6l-1.9-6.638Zm0,0" transform="translate(0 -94.181)" />
              </g>
            </svg>
          </div>
          <nav className="mt-2">
            <Link href="/">
              <a className="flex text-white py-3">
                <svg className="w-6 h-6" fill="none" stroke="currentColor" strokeWidth="1.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.5 21.5">
                  <g transform="translate(-2.25 -1.25)">
                    <path d="M3,9l9-7,9,7V20a2,2,0,0,1-2,2H5a2,2,0,0,1-2-2Z" />
                    <path d="M9,22V12h6V22" />
                  </g>
                </svg>
                <span className="ml-4">
                  Home
                </span>
              </a>
            </Link>
            <Link href="/profile">
              <a className="flex text-white py-3">
                <svg className="w-6 h-6" fill="none" stroke="currentColor" strokeWidth="1.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17.5 19.5">
                  <g transform="translate(-3.25 -2.25)">
                    <path d="M20,21V19a4,4,0,0,0-4-4H8a4,4,0,0,0-4,4v2" />
                    <circle cx="4" cy="4" r="4" transform="translate(8 3)" />
                  </g>
                </svg>
                <span className="ml-4">
                  My Profile
                </span>
              </a>
            </Link>
            <div className="py-3">
              <button className="bg-blue w-full rounded-full text-white py-3">
                Start Ranting
              </button>
            </div>
          </nav>
        </div>
      </div>

      <main className="flex-1 bg-gray-dark">
        <div className="border-b border-gray-700">
          <h1 className="text-xl text-white font-bold px-8 py-4">{title}</h1>
        </div>
        {children}
      </main>

      <div className="w-96 bg-gray-dark border-l border-gray-700"></div>
    </div>
  )
}

Finally, we can use the layout component everywhere:

export default function Home({ tweets }) {
  return (
    <Layout title="Home">
      <h2 className="text-white">My feed...</h2>
    </Layout>
  )
}
export default function Profile({ user }) {
  return (
    <Layout title={user.name || 'Profile'}>
      <h2 className="text-white">My tweets...</h2>
    </Layout>
  )
}

4. Active links

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment