Frontend DevelopmentVueJs Tutorials

Building a Simple Forum With Vue-js, Vuex and Firebase Part3: Home Forums and Replies

building forum with vuejs and firebase

In the final part of this tutorial we will demonstrate how to display the latest forums on the homepage and displaying specific forum and topic also how to add replies on topics and increment the topic view count.

 

 

Series Topics:

 

Modifying Api and Store

We will add some other new functions to the api and store, so open helper/api.js and modify it as shown below:

import firebase from "firebase";

// add your own config params, get them from 
// https://console.firebase.google.com/project/<project-name>/settings/general/
var config = {
  apiKey: "<your api key>",
  authDomain: "<your auth domain>",
  databaseURL: "<your database url>",
  storageBucket: "<your storage bucket>",
};

firebase.initializeApp(config);

var db = firebase.database();

export default {

	authenticate(email, password, successcallback, errorcallback) {
		firebase.auth().signInWithEmailAndPassword(email, password).then(successcallback).catch(errorcallback);
	},
	logout(successcallback, errorcallback) {
		firebase.auth().signOut().then(successcallback).catch(errorcallback);
	},
	getCurrentUser(callback) {
		firebase.auth().onAuthStateChanged(callback);

	},
	register(email, password, successcallback, errorcallback) {
		firebase.auth().createUserWithEmailAndPassword(email, password).then(successcallback).catch(errorcallback);
	},
	updateUserDisplayname(name) {
		var user = firebase.auth().currentUser;

		user.updateProfile({
  			displayName: name,
		}).then(function() {
  			// Update successful.
		}).catch(function(error) {
  			// An error happened.
		});
	},
	addUser(name, email, uid) {
		var usersRef = db.ref('users');
		var usersPush = usersRef.push();

		const key = usersPush.getKey();

		usersPush.set({
			name: name,
			email: email,
			uid: uid, 
			created_at: (new Date()).toLocaleString()
		});

		return key;
	},
    getUserByUID(UID, callback) {
		var userRef = db.ref('users').orderByChild("uid").equalTo(UID);

		userRef.on('value', function(snapshot) {
			if(snapshot.val() != null) {
				callback(Object.keys(snapshot.val())[0], snapshot.val());
			} else {
				callback(null, null);
			}
		});
	},
	addForum(title, content, user_id) {
		var forumRef = db.ref('forums');
		var forumPush = forumRef.push();

		forumPush.set({
			title: title,
			content: content, 
			user_id: user_id,
			created_at: (new Date()).toLocaleString()
		});
	},
	updateForum(title, content, key) {

		db.ref("forums/" + key).update({ title: title, content: content });
	},
	deleteForum(key) {
		db.ref("forums/" + key).remove();
	},
	userForums(user_id, callback) {
		var forumRef = db.ref('forums').orderByChild("user_id").equalTo(user_id);

		forumRef.on('value', function(snapshot) {
			callback(snapshot.val());
		});
	},
	getForumByKey(key, callback) {

		var itemRef = db.ref('forums').child(key);

		var topicsRef = db.ref('topics').orderByChild("forum_id").equalTo(key);

		itemRef.once('value', function(snapshot) {
			callback(snapshot.val(), 'forum')
		});

		topicsRef.on('value', function(snapshot) {
			callback(snapshot.val(), 'topics')
		});
	},
	getMyTopics(forum_key, user_key, callback) {
		var topicsRef = db.ref('topics').orderByChild("userId_forumId").equalTo(user_key + "_" + forum_key);

		topicsRef.on('value', function(snapshot) {
			callback(snapshot.val());
		});
	},
	addTopic(title, content, forum_id, user_id) {
		var forumRef = db.ref('topics');
		var forumPush = forumRef.push();

		forumPush.set({
			title: title,
			content: content, 
			user_id: user_id,
			created_at: (new Date()).toLocaleString(),
			forum_id: forum_id,
			view_count: 0,
			userId_forumId: user_id + "_" + forum_id
		});
	},
	updateTopic(title, content, key) {

		db.ref("topics/" + key).update({ title: title, content: content });
	},
	deleteTopic(key) {
		db.ref("topics/" + key).remove();
	},
    updateTopicViewCount(key)
	{
		var topicRef = db.ref('topics').child(key).child('view_count');

		topicRef.transaction(function(views) {
  			// if (views) {
    			views = views + 1;
  			// }
  			return views;
		});
	},
	getTopicByKey(key, callback) {

		var topicRef = db.ref('topics').child(key);

		topicRef.on('value', function(snapshot) {

			var topicVal = snapshot.val()

			var userRef = db.ref('users').child(topicVal.user_id)

			userRef.on('value', function(snapshotUser) {

				var forumRef = db.ref('forums').child(topicVal.forum_id)

				forumRef.on('value', function(snapshotForum) {

					callback(topicVal, snapshotUser.val(), snapshotForum.val())
				});
			});
		});
	},
	getHomeLatestForums(callback)
	{
		var ref = db.ref("forums");

		ref.once("value", function(snapshot) {

			if(snapshot.val() != null) {
	   			snapshot.forEach(function(forum) {
	   				var forumKey = forum.key;
	   				var forumData = forum.val();
	   				forumData.key = forumKey;
	   				forumData.topics = [];

	   				var topicsRef = db.ref("topics").orderByChild("forum_id").equalTo(forumKey);
	    
	 				
	 				topicsRef.on("child_added", function(topics) {
	  					//console.log(snapshot.key);

	  					var topicsData = topics.val();
	  					topicsData.key = topics.key;
	  					topicsData.username = "";

	   					var usersRef = db.ref("users/" + topicsData.user_id);

	   					usersRef.on("value", function(user) {
	   						topicsData.username = user.val().name;

	   						forumData.topics.push(topicsData);
	   					});
					});
	    			callback(forumData);
	  			});
			} else {
				callback(null);
			}

		}, function (error) {
   			console.log("Error: " + error.code);
		});
	},
	getRepliesByTopicKey(key, callback)
	{
		var repliesRef = db.ref('replies').orderByChild("topic_id").equalTo(key)

		repliesRef.on('value', function(snapshot) {

			if(snapshot.val() != null) {

				snapshot.forEach(function(reply) {

					var repliesData = reply.val();
					repliesData.username = "";
	   				
	   				db.ref("users/" + repliesData.user_id).once("value", function(user) {
						repliesData.username = user.val().name;

						callback(repliesData);
	   				});
				});
		    } else {
		    	callback(null);
		    }
		});
	},
	updateRepliesByTopicKey(key, callback)
	{
		var repliesRef = db.ref('replies').orderByChild("topic_id").equalTo(key)

		repliesRef.on('child_added', function(snapshot) {

			if(snapshot.val() != null) {

				var repliesData = snapshot.val();
					repliesData.username = "";

				db.ref("users/" + repliesData.user_id).once("value", function(user) {
						repliesData.username = user.val().name;

						callback(repliesData);
	   				});
		    } else {
		    	callback(null);
		    }
		});
	},
	addReply(content, topic_id, user_id) {
		var replyRef = db.ref('replies');
		var replyPush = replyRef.push();

		replyPush.set({
			content: content, 
			user_id: user_id,
			created_at: (new Date()).toLocaleString(),
			topic_id: topic_id
		});
	}

}

 

open store.js and modify it as shown below:

import Vue from 'vue'
import Vuex from 'vuex'
import api from './helper/api'

Vue.use(Vuex);

export default new Vuex.Store(
	{
		state: {
			auth: {
				email: "",
				password: "",
				name: ""
			},
			currentUser: {
				id: "",       // this id is the datebase key for this record
				name: "",
				email: "",
				uid: "",      // this is is the user authenticated object
				status: 0   // 0=logout 1=login
			},
			forum: {         // this object will be used when adding and editing forum
				title: "",
				content: ""
			},
			userForums: [],     // user forums in case he is login
			userTopics: [],
			topic: {
				title: "",
				content: ""
			},
			homeForums: [],
			forumDetails: {},
			forumTopics: {},
			topicDetails: {},
			userDetails: {},
                        replies: [],
			reply: ""
		},
		mutations: {
			setAuthEmail(state, data) {
				state.auth.email = data
			},
			setAuthPassword(state, data) {
				state.auth.password = data
			},
			setAuthName(state, data) {
				state.auth.name = data
			},
			setCurrUserId(state, data) {
				state.currentUser.id = data
			},
			setCurrUserName(state, data) {
				state.currentUser.name = data
			},
			setCurrUserEmail(state, data) {
				state.currentUser.email = data
			},
			setCurrUserUid(state, data) {
				state.currentUser.uid = data
			},
			setCurrUserStatus(state, data) {
				state.currentUser.status = data
			},
			setForumTitle(state, data) {
				state.forum.title = data
			},
			setForumContent(state, data) {
				state.forum.content = data
			},
			setUserForums(state, data) {
				state.userForums = data
			},
			setUserTopics(state, data) {
				state.userTopics = data
			},
			setTopicTitle(state, data) {
				state.topic.title = data
			},
			setTopicContent(state, data) {
				state.topic.content = data
			},
			addToHomeForums(state, data) {
				if(data != null && data != "") {
					state.homeForums.push(data);
				}
			},
			setHomeForums(state, data) {
				state.homeForums = data;
			},
			setForumDetails(state, data) {
				state.forumDetails = data
			},
			setForumTopics(state, data) {
				state.forumTopics = data
			},
			setTopicsDetails(state, data) {
				state.topicDetails = data
			},
                        setUserDetails(state, data) {
				state.userDetails = data
			},
			setReplies(state, data) {
				state.replies = data;
			},
			setReply(state, data) {
				state.reply = data
			},
			addToReplies(state, data) {
				if(data != null && data != "") {
					state.replies.push(data);
				} 
			}
		},
		actions: {
			getCurrentUser({commit}) {
				api.getCurrentUser(function(user) {
					if(user) {

						api.getUserByUID(user.uid, function(key, val) {
							if(key != null && val != null) {
								commit('setCurrUserId', key);
								commit('setCurrUserName', user.displayName);
								commit('setCurrUserEmail', user.email);
								commit('setCurrUserUid', user.uid);
								commit('setCurrUserStatus', 1);
							}
						})
					} 
				}); 
			},
			clearUserData({commit}) {
				commit('setCurrUserId', '');
				commit('setCurrUserName', '');
				commit('setCurrUserEmail', '');
				commit('setCurrUserUid', '');
				commit('setCurrUserStatus', 0);

				commit('setAuthEmail', '');
				commit('setAuthPassword', '');
				commit('setAuthName', '');
			},
			getUserForums({commit}, user) {
				api.userForums(user.id, function(response) {
					if(response) {
						commit('setUserForums', response); 
					} else {
						commit('setUserForums', []);
					}
				});
			},
			setHomeForumsOnLoad({commit, dispatch}, callback) {

				commit('setHomeForums', [])

				api.getHomeLatestForums(function(response) {
					commit('addToHomeForums', response)

					callback(response);
				});
			},
			loadForumDetails({commit, dispatch}, payload) {

				api.getForumByKey(payload.route.params.id, function(response, item) {
					
					if(item == 'forum') {
						commit('setForumDetails', response)
					} else {

						commit('setForumTopics', response)
					}

					setTimeout(
						() => document.getElementById("loading").style.display = "none",
						500 
					)
				});
			},
			loadTopicDetails({commit, dispatch}, payload) {

			   api.getTopicByKey(payload.route.params.id, function(topic, user, forum) {
					
					commit('setTopicsDetails', topic)

					commit('setUserDetails', user)

					commit('setForumDetails', forum)

					setTimeout(
						() => document.getElementById("loading").style.display = "none",
						1000 
					)
				});

			   commit('setReplies', [])

				api.updateRepliesByTopicKey(payload.route.params.id, function(response) {

					commit('addToReplies', response)
				});
			}
		}
	}
)

 

Displaying Home Forums

Let’s display the latest forums in the homepage, Open components/pages/Home.vue and insert the below code:

<template>
	<div>
        <div v-if="latestForms.length > 0">
		      <section v-for="(val) in latestForms" class="row panel-body">
            <section class="col-md-6">
              <h4> <router-link :to="'/forum-display/' + val.key"><i class="glyphicon glyphicon-th-list"> </i> {{ val.title }}</router-link></h4> <hr>
              <h6>{{ val.content }} </h6>
              
            </section>

            <section class="col-md-2">
              <ul id="post-topic">
                <li class="list-unstyled"> Topics:{{ val.topics.length }} </li>
              </ul>
            </section>
            <section class="col-md-3" v-if="val.topics.length > 0">
              <h4> <router-link :to="'/topic-display/' + val.topics[0].key"><i class="glyphicon glyphicon-link"> </i> {{ val.topics[0].title }} </router-link></h4> <hr>
              <a href="#"><i class="glyphicon glyphicon-user"></i> {{ val.topics[0].username }} </a><br>
              <a href="#"><i class="glyphicon glyphicon-calendar"></i> {{ val.topics[0].created_at }} </a>
            </section>
           
          </section>
        </div>
        <div v-else>
          <section class="row panel-body">
            <p class="text-center">No forums exit yet! <router-link to="/add-forum/">create new forum</router-link></p>
          </section>
        </div>
	</div>
</template>

<script>

	export default {
     data() {
        return {
        }
     },
     computed: {
        latestForms() {
          return this.$store.state.homeForums;
        }
     },
     mounted() {
        this.$store.dispatch('setHomeForumsOnLoad', function(response) {
            setTimeout(
              () => document.getElementById("loading").style.display = "none",
              500 
            )
        });
     }
	}
</script>

In the above code we dispatched the action setHomeForumsOnload which in turn load and fetches the forums, then we iterate over them, keep in mind when testing for these scenarios that the fetching process may take some time according to your network connection.

 



Displaying Single Forum

At this point we need a way to display single forum and their topics when clicking over them so we will update to files components/pages/ForumDisplay.vue:

<template>
	<div>
		<section class="row panel-body">
	        <section class="col-md-12">
	          <h2> <i class="glyphicon glyphicon-th-list"> </i> {{ forumDetails.title!=undefined?forumDetails.title:"" }}</h2> <hr>
	          <h6>{{ forumDetails.content!=undefined?forumDetails.content:"" }} </h6>
	          
	        </section>

    	</section>

		<hr/>

    	<div class="container-fluid" v-if="forumTopics!='undefined' && forumTopics!=null">
    		<h3>Topics</h3>
    		<ul class="list-group">
				<forum-topics v-for="(value, key, index) in forumTopics" :key="index" :topic="value" :objKey="key"></forum-topics>
			</ul>
		</div>

	</div>
</template>

<script>

	import ForumTopics from '../partials/ForumTopics.vue'
	import api from '../../helper/api'

	export default {
		data() {
			return {
			}
		},
		components: {
			ForumTopics
		},
		methods: {
		
		},
		computed: {
			forumDetails() {
				return this.$store.state.forumDetails
			},
			forumTopics() {
				return this.$store.state.forumTopics
			}
		},
		mounted() {
			this.$store.dispatch('loadForumDetails', {route: this.$route})
		}
	}
</script>

Open components/partials/ForumTopics.vue and modify it like shown:

<template>
	 <li class="list-group-item ">
	 	<span class="badge">Views: {{ topic.view_count }}</span>
		<router-link :to="'/topic-display/' + objKey ">
	    	{{ topic.title }}    
	    </router-link>
		<span> <small>Posted in</small> <i class="glyphicon glyphicon-calendar"></i> {{ topic.created_at }}</span>

	</li>
</template>

<script>

	export default {
		props: ['topic', 'objKey'],
		components: {
		},
		computed: {
		},
		mounted() {
		}
	}
</script>

 

Displaying Single Topic and Replies

Open components/pages/TopicDisplay.vue and update it like shown below:

<template>
	<div>
		<section class="row panel-body" v-if="topicDetails!=null && topicDetails!='undefined' && userDetails!=null && userDetails!='undefined' ">
	        <section class="col-md-12">
	          <h2><i class="glyphicon glyphicon-link"> </i> {{ topicDetails.title }} <small> <router-link :to="'/forum-display/' + topicDetails.forum_id "> {{ forumDetails.title }}</router-link></small></h2>  <hr>
	          
	          <a href="#"><i class="glyphicon glyphicon-user"></i> {{ userDetails.name }} </a><br>
              <i class="glyphicon glyphicon-calendar"></i> {{ topicDetails.created_at }}
				<br/>
			  <span class="badge">Views: {{ topicDetails.view_count }}</span>

			  <hr/>

	          <p>{{ topicDetails.content }}</p>
	          
	        </section>

    	</section>

		<hr/>

		<div class="container-fluid" v-if="replies!='undefined' && replies!=null && replies.length > 0">
			<h3>Replies</h3>
			<topic-replies v-for="(val, index) in replies" :key="index" :reply="val"></topic-replies>
		</div>

		<div class="container-fluid" v-if="this.$store.state.currentUser.status==1">
			<h3>Add Reply</h3>
			<add-reply></add-reply>
		</div>

		<div class="container-fluid" v-else>
			<p class="text-center"><router-link to="/login/">Login</router-link> to add reply</p>
		</div>

	</div>
</template>

<script>

	import TopicReplies from '../partials/TopicReplies.vue'
	import AddReply from '../partials/AddReply.vue'
	import api from '../../helper/api'

	export default {
		components: {
			TopicReplies,
			AddReply
		},
		computed: {
			forumDetails() {
				return this.$store.state.forumDetails
			},
			topicDetails() {
				return this.$store.state.topicDetails
			},
			userDetails() {
				return this.$store.state.userDetails
			},
			replies() {
				return this.$store.state.replies.reverse()
			}
		},
		mounted() {
			// load topic details
			this.$store.dispatch('loadTopicDetails', {route: this.$route})

			// increment view count
			api.updateTopicViewCount(this.$route.params.id)
		}
	}
</script>

Open components/pages/TopicDisplay.vue

<template>
	<div>
		<section class="row panel-body" v-if="topicDetails!=null && topicDetails!='undefined' && userDetails!=null && userDetails!='undefined' ">
	        <section class="col-md-12">
	          <h2><i class="glyphicon glyphicon-link"> </i> {{ topicDetails.title }} <small> <router-link :to="'/forum-display/' + topicDetails.forum_id "> {{ forumDetails.title }}</router-link></small></h2>  <hr>
	          
	          <a href="#"><i class="glyphicon glyphicon-user"></i> {{ userDetails.name }} </a><br>
              <i class="glyphicon glyphicon-calendar"></i> {{ topicDetails.created_at }}
				<br/>
			  <span class="badge">Views: {{ topicDetails.view_count }}</span>

			  <hr/>

	          <p>{{ topicDetails.content }}</p>
	          
	        </section>

    	</section>

		<hr/>

		<div class="container-fluid" v-if="replies!='undefined' && replies!=null && replies.length > 0">
			<h3>Replies</h3>
			<topic-replies v-for="(val, index) in replies" :key="index" :reply="val"></topic-replies>
		</div>

		<div class="container-fluid" v-if="this.$store.state.currentUser.status==1">
			<h3>Add Reply</h3>
			<add-reply></add-reply>
		</div>

		<div class="container-fluid" v-else>
			<p class="text-center"><router-link to="/login/">Login</router-link> to add reply</p>
		</div>

	</div>
</template>

<script>

	import TopicReplies from '../partials/TopicReplies.vue'
	import AddReply from '../partials/AddReply.vue'
	import api from '../../helper/api'

	export default {
		components: {
			TopicReplies,
			AddReply
		},
		computed: {
			forumDetails() {
				return this.$store.state.forumDetails
			},
			topicDetails() {
				return this.$store.state.topicDetails
			},
			userDetails() {
				return this.$store.state.userDetails
			},
			replies() {
				return this.$store.state.replies.reverse()
			}
		},
		mounted() {
			// load topic details
			this.$store.dispatch('loadTopicDetails', {route: this.$route})

			// increment view count
			api.updateTopicViewCount(this.$route.params.id)
		}
	}
</script>

Open components/partials/TopicReplies.vue

<template>
		<div class="panel panel-default">
	    	<div class="panel panel-heading">
                {{ reply.username }}

                <span class="pull-right">{{ reply.created_at }}</span>
            </div>

            <div class="panel panel-body">
                {{ reply.content }}
            </div>    
	    </div>

</template>

<script>

	export default {
		props: ['reply'],
		components: {
		}
	}
</script>

Open components/partials/AddReply.vue

<template>
	<div>
		<form method="POST" v-on:submit.prevent="addReply">
			<div class="panel panel-default">
				<div class="panel panel-header">
					<strong>Use any of these emotions: </strong>
					<ul class="emotion-list">
						<li v-for="(val, index) in emotionList" :key="index" v-html="val" @click="insertEmo">
							
						</li>
					</ul>
				</div>
	            <div class="panel panel-body">
	            	
	                	<textarea rows="3" name="reply" class="form-control" v-model="reply"></textarea>
	            </div>
	            <div class="panel panel-footer">
	            	<button type="submit" class="btn btn-success" :disabled="disabled()"><i v-show="isLoading" class="fa fa-refresh fa-lg fa-spin btn-load-indicator"></i> Add</button>	
	            </div>    
		    </div>
	    </form>
	</div>
</template>

<script>

	import api from '../../helper/api'

	export default {
		data() {
			return {
				isLoading: false,
				emotionList: [
					"😁",
					"😂",
					"😃",
					"😅",
					"😆",
					"😉",
					"😊",
					"😋",
					"😌",
					"😍",
					"😏",
					"😒",
					"😓",
					"😔",
					"😖",
					"😘",
					"😚",
					"😜",
					"😝",
					"😞",
					"😠",
					"😡",
					"😢",
					"😣",
					"😤",
					"😥",
					"😨",
					"😩",
					"😪",
					"😫",
					"😭",
					"😰",
					"😱",
					"😲",
					"😳",
					"😵",
					"😷",
					"😸",
					"😹",
					"😺",
					"😻",
					"😼",
					"😽",
					"😿",
					"🙀",
					"🙅",
					"🙆",
					"🙇",
					"🙈"
				]
			}
		},
		computed: {
			reply: {
				get() {
					return this.$store.state.reply
				},
				set(value) {
					this.$store.commit('setReply', value)
				}
			}
		},
		methods: {
			disabled() {
				if(this.$store.state.reply == "") {
					return true
				} 

				return false
			},
			insertEmo(e) {
				let reply = this.$store.state.reply;

				reply += e.target.innerHTML;

				this.$store.commit('setReply', reply)
			},
			addReply() {
				var self = this;

				if(self.$store.state.reply != "") {

					self.isLoading = true

					api.addReply(self.$store.state.reply, self.$route.params.id, self.$store.state.currentUser.id)

					setTimeout(
						() => { 
							
							self.$store.commit('setReplies', [])

							// reload replies
							api.updateRepliesByTopicKey(self.$route.params.id, function(response) {

								self.$store.commit('addToReplies', response)

								self.isLoading = false
							});

							self.$store.commit('setReply', "")
					},
						1500
					)

				}
			}
		}
	}
</script>

Now with these the above code the whole buzzle is completed try to refresh the page and click on forum and topic links, also try to view replies and add replies.

 

Conclusion

Upon following this series you have learned a lot of topics including vuejs development and how to use Vuex store to store state data and also the ability to move to pages using Vue router, and how to connect to firebase and use it’s api for storing data and authentication

3.7 3 votes
Article Rating

What's your reaction?

Excited
5
Happy
5
Not Sure
4
Confused
7

You may also like

Subscribe
Notify of
guest

3 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Bionictoe
Bionictoe
4 years ago

Is this on Github?

Reece James
1 year ago

All clear. Thank you very much for the information. I will send this article to my friend from Factor Dedicated Teams