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.
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.
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.
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);
});
},
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.
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;
- },
- },
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;
}
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;
+ },
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>
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.
With these changes, you should be able to view a user's page from the search results.
Now we're going to make it possible to follow other users.
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
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.
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.
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
.
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;
}
}
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.
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.