Day 3 - BYU-CS260-Winter-2018/redbird GitHub Wiki

For Day 3, we will add add the ability to follow users and display their tweets in your feed.

Viewing Users

To be able to follow a user, we need to be able to view the user's page. We'll modify the server to let us look up an individual user, then create Vue components to view that user.

Modify the server

First, let's modify server.js to add a REST endpoint for getting a user:

app.get('/api/users/:id', (req, res) => {
  let id = parseInt(req.params.id);
  // get user record
  knex('users').where('id',id).first().select('username','name','id').then(user => {
    res.status(200).json({user:user});
   }).catch(error => {
     res.status(500).json({ error });
   });
 });

While we are editing the server, let's also modify the search endpoint so we return user IDs:

app.get('/api/tweets/search', (req, res) => {
     .orderBy('created','desc')
     .limit(limit)
     .offset(offset)
-    .select('tweet','username','name','created').then(tweets => {
+    .select('tweet','username','name','created','users.id as userID').then(tweets => {
       res.status(200).json({tweets:tweets});
     }).catch(error => {
       console.log(error);

The same for the hashtag search endpoint:

app.get('/api/tweets/hash/:hashtag', (req, res) => {
     .orderBy('created','desc')
     .limit(limit)
     .offset(offset)
-    .select('tweet','username','name','created').then(tweets => {
+    .select('tweet','username','name','created','users.id as userID').then(tweets => {
       res.status(200).json({tweets:tweets});

These changes will let us link from the search pages to a user's account page.

Modify the Store

Now we need to modify the store to use these changes. First, add some new data properties:

     loginError: '',
     registerError: '',
     feed: [],
+    userView: [],
+    feedView: [],

Then add some getters for these properties:

    feedView: state => state.feedView,
    userView: state => state.userView,

And add some setters as well:

    setUserView (state, user) {
      state.userView = user;
    },
    setFeedView (state, feed) {
      state.feedView = feed;
    },

Now add an action to get a user:

    // Users //
    // get a user, must supply {username: username} of user you want to get
    getUser(context,user) {
      return axios.get("/api/users/" + user.id).then(response => {
	context.commit('setUserView',response.data.user);
      }).catch(err => {
	console.log("getUser failed:",err);
      });
    },

We also need an action to get a user's tweets so we can see if we are interested in following them:

    // get tweets of a user, must supply {id:id} of user you want to get tweets for
    getUserTweets(context,user) {
      return axios.get("/api/users/" + user.id + "/tweets").then(response => {
	context.commit('setFeedView',response.data.tweets);
      }).catch(err => {
	console.log("getUserTweets failed:",err);
      });
    },

Create user pages

To follow a user, we need to view the page for her account. Let's make a new component in src/components/UserPage.vue. Here is the template:

<template>
  <div class="feed">
    <h2>
      {{userView.name}} @{{userView.username}}
    </h2>
    <feed-list v-bind:feed="feed" />
  </div>
</template>

Note, like other pages that show a list of tweets, we're going to use the FeedList component. However, this component currently fetches the logged in user's tweets. We need to instead to fetch this user's tweets. We're going to have to refactor the FeedList component so that we can pass it a list of tweets to display. We'll do this by binding the feed parameter to the feed property.

Here is the script portion of this component:

<script>
 import FeedList from './FeedList';
 export default {
   name: 'UserPage',
   components: { FeedList },
   created: function() {
     this.$store.dispatch('getUser',{id:this.$route.params.userID});
     this.$store.dispatch('getUserTweets',{id:this.$route.params.userID});
   },
   computed: {
     user: function() {
       return this.$store.getters.user;
     },
     userView: function() {
       return this.$store.getters.userView;
     },
     feed: function() {
       return this.$store.getters.feedView;
     },
   },
 }
</script>

Notice that we have to import this component and list it as a child component.

Refactor FeedList.vue

Modify src/components/FeedList.vue so it can link to user pages:

 <template>
   <div>
     <div v-for="item in feed" class="item">
-      <p class="idline"><span class="user">{{item.name}}</span><span class="handle">@{{item.username}}</span><span class="time">{{item.created | since}}</span></p>
+      <p class="idline"><span class="user">{{item.name}}</span><router-link :to="{ name: 'UserPage', params: {userID: item.userID}}"><span class="handle">@{{item.username}}</span></router-link><span class="time">{{item.created | since}}</span></p>
       <p v-html="formatTweet(item.tweet)" class="tweet"></p>
     </div>
   </div>

We also need to modify this component so it can receive a propety from its parent components:

  import linkify from './linkify.js';
  export default {
    name: 'FeedList',
+   props: ['feed'],
    filters: {
      since: function(datetime) {
        moment.locale('en', {

And we need to remove the part where it loads the feed itself:

-   computed: {
-     feed: function() {
-       return this.$store.getters.feed;
-     },
-   },

Refactor SearchResults.vue

Modify src/components/SearchResults.vue so that it can pass a list of tweets to the FeedList, just like UserPage. Here is the template change:

  <div class="feed">
     <h1>Search Results</h1>
     <p>Searched for {{keywords}}</p>
-    <feed-list/>
+    <feed-list v-bind:feed="feed" />
   </div>
 </template>

Here is the computed property change:

      this.$store.dispatch('doSearch',this.$route.query.keywords);
    },
    computed: {
+     feed: function() {
+       return this.$store.getters.feed;
+     },
      keywords: function() {
        return this.$route.query.keywords;
      }

Refactor HashTag.vue and UserFeed.vue

Modify src/components/HashtTag.vue and src/components/UserFeed.vue in the same way. Here is the template change:

-    <feed-list/>
+    <feed-list v-bind:feed="feed" />

Here is the computed property change:

   computed: {
+     feed: function() {
+      return this.$store.getters.feed;
+     },

Update the menu

Modify src/components/AppHeader.vue so that we can link to a user's own page:

-      <li class="right" v-if="loggedIn"><a @click="logout" href="#">Logout</a></li>
-      <li class="right" v-if="loggedIn">{{user.username}}</li>
+      <li class="right" v-if="loggedIn">
+       <router-link :to="{ name: 'UserPage', params: {userID: user.id}}">{{user.username}}</router-link> <a @click="logout" href="#">Logout</a></p>
+      </li>

Update the Router

We need to modify the router to visit the user pages. Modify src/router/index.js to import the new component:

import UserPage from '@/components/UserPage'

In addition, add the new route:

+    {
+      path: '/user/:userID',
+      name: 'UserPage',
+      component: UserPage
+    },

Note that this is a parameterized route, just like the HashTag component.

Test It

With these changes, you should be able to view a user's page from the search results.

Followers

Now we're going to make it possible to follow other users.

Database

We need migration that modifies our database to keep track of followers:

npx knex migrate:make followers

Edit the migration that was created and add this:

exports.up = function(knex, Promise) {
    return Promise.all([
    knex.schema.createTable('followers', function(table) {
      table.increments('id').primary();
      table.integer('user_id').unsigned().notNullable().references('id').inTable('users');
      table.integer('follows_id').unsigned().notNullable().references('id').inTable('users');
    }),
  ]);
};

exports.down = function(knex, Promise) {
  return Promise.all([
    knex.schema.dropTable('followers'),
  ]);
};

This creates a separate table, called followers, that maps a user ID to the ID of the person they are following. There can be many entries for a given user in this table, so it is possible for one person to follow many people, and for many people to follow one person. This is called a many-to-many relationship.

Let's run this migration:

npx knex migrate:latest

REST API

We will edit server.js to add REST API endpoints for following people. First, we need an endpoint to follow someone:

// follow someone
app.post('/api/users/:id/follow', (req,res) => {
  // id of the person who is following
  let id = parseInt(req.params.id);
  // id of the person who is being followed
  let follows = req.body.id;
  // make sure both of these users exist
  knex('users').where('id',id).first().then(user => {
    return knex('users').where('id',follows).first();
  }).then(user => {
    // make sure entry doesn't already exist
    return knex('followers').where({user_id:id,follows_id:follows}).first();
  }).then(entry => {
    if (entry === undefined)
      // insert the entry in the followers table
      return knex('followers').insert({user_id: id, follows_id:follows});
    else
      return true;
  }).then(ids => {
    res.sendStatus(200);
    return;
  }).catch(error => {
    console.log(error);
    res.status(500).json({ error });
  });
});

Notice that we need to check that both users exist, and that this following relationship isn't already in the database.

Then, we need an endpoint to unfollow someone:

// unfollow someone
app.delete('/api/users/:id/follow/:follower', (req,res) => {
  // id of the person who is following
  let id = parseInt(req.params.id);
  // id of the person who is being followed
  let follows = parseInt(req.params.follower);
  // make sure both of these users exist
  knex('users').where('id',id).first().then(user => {
    return knex('users').where('id',follows).first();
  }).then(user => {
    // delete the entry in the followers table
    return knex('followers').where({'user_id':id,follows_id:follows}).first().del();
  }).then(ids => {
    res.sendStatus(200);
    return;
  }).catch(error => {
    console.log(error);
    res.status(500).json({ error });
  });
});

Again, we make sure both users exist before deleting the following relationship.

We need an endpoint to get a list of people someone is following:

// get list of people you are following
app.get('/api/users/:id/follow', (req,res) => {
  // id of the person we are interested in
  let id = parseInt(req.params.id);
  // get people this person is following
  knex('users').join('followers','users.id','followers.follows_id')
    .where('followers.user_id',id)
    .select('username','name','users.id').then(users => {
      res.status(200).json({users:users});
    }).catch(error => {
      console.log(error);
      res.status(500).json({ error });
    });
});

This is a simple join on the users and followers tables.

And an endpoint to get a list of people who are following you:

// get list of people who are following you
app.get('/api/users/:id/followers', (req,res) => {
  // id of the person we are interested in
  let id = parseInt(req.params.id);
  // get people who are following of this person
  knex('users').join('followers','users.id','followers.user_id')
    .where('followers.follows_id',id)
    .select('username','name','users.id').then(users => {
      res.status(200).json({users:users});
    }).catch(error => {
      console.log(error);
      res.status(500).json({ error });
    });
});

This is also a simple join.

Finally, we want an endpoint to get the list of tweets from people you are following (plus your own. This is commonly known as a "feed".

// get the tweets of those you are following
// use limit to limit the results to a certain number
// use offset to provide an offset into the results (e.g., starting at results number 10)
app.get('/api/users/:id/feed', (req,res) => {
  // id of the person we are interested in
  let id = parseInt(req.params.id);
  // offset into the results
  let offset = 0;
  if (req.query.offset)
    offset = parseInt(req.query.offset);
  // number of results we should return
  let limit = 50;
  if (req.query.limit)
    limit = parseInt(req.query.limit);
  // get people this person is following
  knex('followers').where('followers.user_id',id).then(followed => {
    // get tweets from this users plus people this user follows
    let following = followed.map(entry=>entry.follows_id);
    following.push(id);
    return knex('tweets').join('users','tweets.user_id','users.id')
      .whereIn('tweets.user_id',following)
      .orderBy('created','desc')
      .limit(limit)
      .offset(offset)
      .select('tweet','username','name','created','users.id as userID');
  }).then(tweets => {
    res.status(200).json({tweets:tweets});
  }).catch(error => {
    console.log(error);
    res.status(500).json({ error });
  });
});

This requires first getting a list of all the people you follow, adding in yourself, and then finding all the tweets from these people. Notice the use of a "WHERE IN" clause in SQL.

The Store

We need to modify our store to access each of these REST endpoints. Edit src/store.js and modify the store to include some new data:

     loginError: '',
     registerError: '',
     feed: [],
+    following: [],
+    followers: [],
+    followingView: [],
+    followersView: [],

These will store the people the logged in user is following, along with who is following that person.

Now add these getters:

    following: state => state.following,
    followers: state => state.followers,
    isFollowing: state => (id) => {
      return state.following.reduce((val,item) => {
       if (item.id === id)
         return true;
       else
         return val;
      },false);
    },
    followingView: state => state.followingView,
    followersView: state => state.followersView,

Notice that we can use the list of people the logged in user is following to check whether they are following a certain person.

We also need some mutations:

    setFollowing (state, following) {
      state.following = following;
    },
    setFollowers (state, followers) {
      state.followers = followers;
    },
    setFollowingView (state, following) {
      state.followingView = following;
    },
    setFollowersView (state, followers) {
      state.followersView = followers;
    },    

We need an action to get a list of people you are following:

    // get list of people you are following
    getFollowing(context) {
      return axios.get("/api/users/" + context.state.user.id + "/follow").then(response => {
	context.commit('setFollowing',response.data.users);	
      }).catch(err => {
	console.log("following failed:",err);
      });
    },

We also need an action to get a list of people who are following you:

    // get list of people who are following you
    getFollowers(context) {
      return axios.get("/api/users/" + context.state.user.id + "/followers").then(response => {
	context.commit('setFollowers',response.data.users);
      }).catch(err => {
	console.log("following failed:",err);
      });
    },

We need similar actions for getting the followers of someone else you are viewing:

    // get list of people you are following
    getFollowingView(context,user) {
      return axios.get("/api/users/" + user.id + "/follow").then(response => {
	context.commit('setFollowingView',response.data.users);	
      }).catch(err => {
	console.log("following failed:",err);
      });
    },
    // get list of people who are following you
    getFollowersView(context,user) {
      return axios.get("/api/users/" + user.id + "/followers").then(response => {
	context.commit('setFollowersView',response.data.users);
      }).catch(err => {
	console.log("following failed:",err);
      });
    },    

Next, add an action to follow someone:

    // follow someone, must supply {id: id} of user you want to follow
    follow(context,user) {
      return axios.post("/api/users/" + context.state.user.id + "/follow",user).then(response => {
       context.dispatch('getFollowing');
      }).catch(err => {
       console.log("follow failed:",err);
      });
    },

We also need an action to unfollow someone:

    // unfollow someone, must supply {id: id} of user you want to unfollow
    unfollow(context,user) {
      return axios.delete("/api/users/" + context.state.user.id + "/follow/" + user.id).then(response => {
       context.dispatch('getFollowing');
      }).catch(err => {
       console.log("unfollow failed:",err);
      });
    },

Finally, we need a method to get the list of tweets for people you follow:

    // get tweets of people you follow
    getFeed(context) {
      return axios.get("/api/users/" + context.state.user.id + "/feed").then(response => {
	context.commit('setFeed',response.data.tweets);
      }).catch(err => {
	console.log("getFeed failed:",err);
      });
    },

Note, we previously had a method called getFeed that got a list of tweets that you had made. Be sure to delete this and replace it with this new function.

User List

We need a component to provide a list of users (such as followers). Create src/components/UserList.vue with the following:

<template>
  <div>
    <div v-for="user in users">
      <p><span class="user">{{user.name}}</span><router-link :to="{ name: 'UserPage', params: {userID: user.id}}"><span class="handle">@{{user.username}}</span></router-link></p>
    </div>
  </div>
</template>

<script>
 export default {
   name: 'UserList',
   props: ['users'],
 }

</script>

<style scoped>
 .item {
     border-bottom: 1px solid #ddd;
     padding: 10px;
 }
 .user {
     font-weight: bold;
     margin-right: 10px;
 }
 .handle {
     margin-right: 10px;
     color: #666;
 }
</style>

This is a child component, so it will be passed a list of users as a prop.

User Pages

Now we need to modify src/components/UserPage.view so we can follow people:

First, in the template add these buttons:

    <h2>
      {{userView.name}} @{{userView.username}}
+      <button class="alternate" v-if="isFollowing" v-on:click="unfollow">Unfollow</button>
+      <button class="alternate" v-else v-on:click="follow">Follow</button>
    </h2>

Also modify the template so it reads as follows:

-    <feed-list v-bind:feed="feed" />
+    <div v-if="showTweets">
+      <button class="alternate" v-on:click="toggle">Show Following</button>
+      <feed-list v-bind:feed="feed" />
+    </div>
+    <div v-else>
+      <button class="alternate" v-on:click="toggle">Show Tweets</button>
+      <h3>Following</h3>
+      <user-list v-bind:users="followingView"/>
+      <h3>Followers</h3>
+      <user-list v-bind:users="followersView"/>
+    </div>

Now modify the properties of the component:

  import FeedList from './FeedList';
+ import UserList from './UserList';
  export default {
    name: 'UserPage',
-   components: { FeedList },
+   components: { FeedList, UserList },
+   data() {
+     return {
+       showTweets: true,
+     }
   },

Add the following in created:

     this.$store.dispatch('getFollowingView',{id:this.$route.params.userID});
     this.$store.dispatch('getFollowersView',{id:this.$route.params.userID});

Now add the following computed properties:

     followingView: function() {
       return this.$store.getters.followingView;
     },
     followersView: function() {
       return this.$store.getters.followersView;
     },
     isFollowing: function() {
       return this.$store.getters.isFollowing(this.userView.id);
     },

Add this watch property:

   watch: {
     '$route.params.userID'() {
       this.$store.dispatch('getUser',{id:this.$route.params.userID});
       this.$store.dispatch('getUserTweets',{id:this.$route.params.userID});
       this.$store.dispatch('getFollowingView',{id:this.$route.params.userID});
       this.$store.dispatch('getFollowersView',{id:this.$route.params.userID});
       this.showTweets = true;
     }
   },

And add these methods for when the buttons are clicked:

   methods: {
     follow: function() {
       this.$store.dispatch('follow',{id:this.userView.id});
     },
     unfollow: function() {
       this.$store.dispatch('unfollow',{id:this.userView.id});
     },
     toggle: function() {
       this.showTweets = !this.showTweets;
     }
   }

Addtweets script

I've modified the addtweets.js script to take into account that we now have a followers table, so you may want to download it again.

Test It

You should now be able to follow different people. And the home page for each user shows the tweets of the people they are following, plus their own tweets.

⚠️ **GitHub.com Fallback** ⚠️