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:
- Building a Simple Forum With Vue-js, Vuex and Firebase Part1: Authentication
- Building a Simple Forum With Vue-js, Vuex and Firebase Part2: Forum and Topic CRUD
- Building a Simple Forum With Vue-js, Vuex and Firebase Part3: Home Forums and Replies
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
Is this on Github?
https://bitbucket.org/webmobtuts/vue-forum
All clear. Thank you very much for the information. I will send this article to my friend from Factor Dedicated Teams