What you’ll be building. Demo, Git Repo Here.

Introduction
App and web development have come a long way over the last few years. We use a lot of social media sites everyday, including Facebook, Twitter, WhatsApp, LinkedIn, and Instagram. One of the most widely used features is live chat. Using the CometChat communications SDK and Firebase backend services, you will learn how to build one of the best social networking sites on the internet with minimal effort.
Follow along the steps to build a Facebook clone that will allow users to add Facebook-like posts, stories, and more on the feed. This tutorial will use VueJs, Firebase, and CometChat to build a Facebook clone with a touch of tailwind's CSS.
Prerequisites
To follow this tutorial, you must have a degree of understanding of the general use of VueJs version three. This will help you to improve your understanding of this tutorial.
Installing The App Dependencies
First, you need to have NodeJs installed on your machine; you can go to their website to do that.
Second, you need to have the React-CLI installed on your computer using the command below.
npm install -g @vue/cli
Next, create a new project with the name facebook-clone and also add vue-router as a preinstall dependency.
vue create facebook-clone
Now, install these essential dependencies for our project using the instruction below.
Install and Configuring Tailwind CSS
To have tailwind CSS properly installed on the project, run the following codes.
npm install -D tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
Next, generate the Tailwind and post CSS configuration files.
npx tailwindcss init -p
This will create two files in your root directory: tailwind.config.js
and postcss.config.js
. The tailwind config file is where you add in customization and theming for your app. It is also where you tell tailwind what paths to search for your pages and components. Configure them in the manner below:
// tailwind.congig.js
module.exports = {
node: 'jit',
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Create a file named index.css in the src directory and paste the following codes:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.icon {
@apply hidden xl:inline-flex p-2 h-10 w-10 bg-gray-200 rounded-full text-gray-700 cursor-pointer hover:bg-gray-300;
}
.inputIcon {
@apply flex items-center space-x-1 hover:bg-gray-200 rounded-full flex-grow justify-center p-2 rounded-xl cursor-pointer;
}
}
Finally, import your entry CSS file in your entry JavaScript file(main.js):
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './index.css' // There you go
Now that we're done with the installations, let's move on to building our facebook-clone app solution.
Installing CometChat SDK
- Head to CometChat Pro and create an account.
- From the dashboard, add a new app called "facebook-clone".
- Select this newly added app from the list.
- From the Quick Start copy the APP_ID, REGION and AUTH_KEY, which will be used later.
- Also copy the REST_API_KEY from the API & Auth Key tab.
- Navigate to the Users tab, and delete all the default users and groups leaving it clean (very important).
- Create a "app.config.js" in the src directory of the project.
- Enter your secret keys from CometChat and Firebase below on the next heading.
- Run the following command to install the CometChat SDK.
npm install @cometchat-pro/chat@2.3.0 --save
The App Config File
The setup below spells out the format for configuring the app.config.js files for this project.
const firebaseConfig = {
apiKey: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx',
authDomain: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx',
databaseURL: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx',
projectId: 'xxx-xxx-xxx',
storageBucket: 'xxx-xxx-xxx-xxx-xxx',
messagingSenderId: 'xxx-xxx-xxx',
appId: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx',
measurementId: 'xxx-xxx-xxx',
},
const cometChatConfig = {
APP_ID: 'xxx-xxx-xxx',
AUTH_KEY: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx',
REST_KEY: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx',
APP_REGION: 'xx',
}
export { firebaseConfig, cometChatConfig }
WARNING: "The REST_KEY should not be exposed in front-end application. It should be part of the server-side environment only. But, here its used for demo purposes".
Setting Up Firebase Project
Head to Firebase to create a new project and activate the email and password authentication service. This is how you do it:
To begin using Firebase, you’ll need a Gmail account. Head over to Firebase and create a new project.
Firebase provides support for authentication using different providers. For example, Social Auth, phone numbers, as well as the standard email and password method. Since we’ll be using the email and password authentication method in this tutorial, we need to enable this method for the project we created in Firebase, as it is by default disabled.
Under the authentication tab for your project, click the sign-in method and you should see a list of providers currently supported by Firebase.
Next, click the edit icon on the email/password provider and enable it.
Now, you need to go and register your application under your Firebase project. On the project’s overview page, select the add app option and pick web as the platform.
Once you’re done registering the application, you’ll be presented with a screen containing your application credentials. Take note of the second script tag as we’ll be using it shortly in our application.
Congratulations, now that you're done with the installations, let's do some configurations.
Configuring CometChat SDK
Inside your project structure, open the main.js & index.css files and paste the codes below.
The below codes initialize CometChat in your app before it spins up. The index.js entry file uses your CometChat API Credentials. The app.config.js file also contains your Firebase Configurations variable file. Please do not share your secret keys on GitHub.
Also, the main.js file does more than bootstrap your application, it is also responsible for ensuring that an unauthenticated user is directed to the login page for authentication.
@tailwind base; | |
@tailwind components; | |
@tailwind utilities; | |
@layer components { | |
.icon { | |
@apply hidden xl:inline-flex p-2 h-10 w-10 bg-gray-200 rounded-full text-gray-700 cursor-pointer hover:bg-gray-300; | |
} | |
.inputIcon { | |
@apply flex items-center space-x-1 hover:bg-gray-200 rounded-full flex-grow justify-center p-2 rounded-xl cursor-pointer; | |
} | |
} |
import { createApp } from "vue"; | |
import App from "./App.vue"; | |
import router from "./router"; | |
import "./index.css"; | |
import { CometChat } from "@cometchat-pro/chat"; | |
import { cometChat } from "./app.config"; | |
router.beforeEach((to, from, next) => { | |
const requiresAuth = to.matched.some(record => record.meta.requiresAuth); | |
const user = JSON.parse(localStorage.getItem("user")); | |
if (requiresAuth && !user) { | |
console.log("You are not authorized to access this area."); | |
next("login"); | |
} else { | |
next(); | |
} | |
}); | |
const appID = cometChat.APP_ID; | |
const region = cometChat.APP_REGION; | |
const appSetting = new CometChat.AppSettingsBuilder() | |
.subscribePresenceForAllUsers() | |
.setRegion(region) | |
.build(); | |
CometChat.init(appID, appSetting) | |
.then(() => { | |
console.log("Initialization completed successfully"); | |
// You can now call login function. | |
createApp(App) | |
.use(router) | |
.mount("#app"); | |
}) | |
.catch(error => { | |
console.log("Initialization failed with error:", error); | |
}); |
Configuring The Firebase File
This file is responsible for interfacing with Firebase authentication and database services. Also, it makes ready our google authentication service provider enabling us to sign in with google.
import firebase from 'firebase/app' | |
import 'firebase/firestore' | |
import 'firebase/storage' | |
import 'firebase/auth' | |
import { firebaseConfig } from './app.config' | |
const firebaseApp = firebase.initializeApp(firebaseConfig) | |
const db = firebaseApp.firestore() | |
const storage = firebaseApp.storage() | |
const auth = firebaseApp.auth() | |
const provider = new firebase.auth.GoogleAuthProvider() | |
const timestamp = firebase.firestore.FieldValue.serverTimestamp() | |
export { auth, storage, provider, timestamp } | |
export default db |
Project Structure
The image below reveals the project structure. Make sure you see the folder arrangement before proceeding.
Now, let's make the rest of the project components as seen in the image above.
The Router File
The router file contains all the relevant codes that navigate the user to different parts of our application. See the codes below.
import { createRouter, createWebHistory } from 'vue-router' | |
const routes = [ | |
{ | |
path: '/', | |
name: 'Wall', | |
component: () => import(/* webpackChunkName: "wall" */ '@/views/Wall.vue'), | |
meta: { requiresAuth: true }, | |
}, | |
{ | |
path: '/users', | |
name: 'users', | |
component: () => | |
import(/* webpackChunkName: "users" */ '@/views/Users.vue'), | |
meta: { requiresAuth: true }, | |
}, | |
{ | |
path: '/groups', | |
name: 'groups', | |
component: () => | |
import(/* webpackChunkName: "groups" */ '@/views/Groups.vue'), | |
meta: { requiresAuth: true }, | |
}, | |
{ | |
path: '/chats/:type/:id', | |
name: 'Chats', | |
component: () => | |
import(/* webpackChunkName: "chats" */ '@/views/Chats.vue'), | |
meta: { requiresAuth: true }, | |
props: true, | |
}, | |
{ | |
path: '/login', | |
name: 'Login', | |
component: () => | |
import(/* webpackChunkName: "login" */ '@/views/Login.vue'), | |
}, | |
] | |
const router = createRouter({ | |
history: createWebHistory(), | |
routes, | |
}) | |
export default router |
The App Component
The App component is responsible for dynamically rendering our components employing the services of the Auth-Guard in the main.js file. The Auth-Guard ensures that only authenticated users are permitted to access our app, thereby providing security for our application.
<template> | |
<div id="app" class="h-screen bg-gray-100 overflow-hidden"> | |
<router-view :key="$route.path"/> | |
</div> | |
</template> | |
<script> | |
export default { | |
name: "app", | |
}; | |
</script> | |
<style scoped> | |
</style> |
The Login Component
This component is responsible for authenticating our users using the Firebase google authentication service. It accepts the user credentials and either signs him up or in, depending on if he is new to our application. See the glitch code below and observe how our app interacts with the CometChat SDK.
<template> | |
<div class="login grid place-items-center p-10"> | |
<img src="../assets/logo.png" alt="Facebook Logo" loading="lazy" /> | |
<div class="flex items-center"> | |
<button | |
class=" | |
my-5 | |
p-3 | |
rounded-full | |
text-white text-center | |
cursor-pointer | |
focus:outline-none | |
" | |
@click="signIn" | |
> | |
{{ !loading ? "Sign In With Google" : "Signing..." }} | |
</button> | |
</div> | |
</div> | |
</template> | |
<script> | |
import { auth, provider } from "../firebase"; | |
import { CometChat } from "@cometchat-pro/chat"; | |
import { cometChat } from "../app.config"; | |
import { ref } from "vue"; | |
import { useRouter } from "vue-router"; | |
export default { | |
name: "login", | |
setup() { | |
let loading = ref(false); | |
const router = useRouter(); | |
const signIn = () => { | |
loading.value = true; | |
auth | |
.signInWithPopup(provider) | |
.then((res) => loginCometChat(res.user)) | |
.catch((error) => { | |
loading.value = false; | |
console.log(error); | |
alert(error.message); | |
}); | |
}; | |
const loginCometChat = (data) => { | |
const authKey = cometChat.AUTH_KEY; | |
CometChat.login(data.uid, authKey) | |
.then((u) => { | |
console.log(u); | |
localStorage.setItem('user', JSON.stringify(data)) | |
loading.value = false; | |
router.push("/"); | |
}) | |
.catch((error) => { | |
if (error.code === "ERR_UID_NOT_FOUND") { | |
signUpWithCometChat(data); | |
} else { | |
console.log(error); | |
loading.value = false; | |
alert(error.message); | |
} | |
}); | |
}; | |
const signUpWithCometChat = (data) => { | |
const authKey = cometChat.AUTH_KEY; | |
const user = new CometChat.User(data.uid); | |
user.setName(data.displayName); | |
user.setAvatar(data.photoURL); | |
CometChat.createUser(user, authKey) | |
.then(() => { | |
loading.value = false; | |
alert("You are now signed up, click the button again to login"); | |
}) | |
.catch((error) => { | |
console.log(error); | |
loading.value = false; | |
alert(error.message); | |
}); | |
}; | |
return { loading, signIn }; | |
} | |
}; | |
</script> | |
<style scoped> | |
.login > img { | |
object-fit: contain; | |
width: 200px; | |
height: 200px; | |
} | |
.login > div > button:first-child { | |
border: 1px solid #45619d; | |
background-color: #45619d; | |
transition: all 0.3s ease-in-out; | |
margin: 20px 5px; | |
} | |
.login > div > button:first-child:hover { | |
background-color: transparent; | |
color: #45619d; | |
} | |
/* .login > div > button:last-child { | |
border: 1px solid #0a8d48; | |
background-color: #0a8d48; | |
transition: all 0.3s ease-in-out; | |
margin: 20px 5px | |
} | |
.login > div > button:last-child:hover { | |
background-color: transparent; | |
color: #0a8d48; | |
} */ | |
</style> |
The Wall View
This is where all the magic happens. This component embodies other sub-components like the MainHeader, Sidebar, Feed, Widget, etc.
As intuitive as they sound, the above sub-components can be best observed in the image below.
Observe the code below to understand how they are all glued with the tailwind CSS.
<template> | |
<div class="wall"> | |
<MainHeader /> | |
<main class="flex"> | |
<Sidebar /> | |
<Feed /> | |
<Widget /> | |
</main> | |
</div> | |
</template> | |
<script> | |
import MainHeader from "../components/MainHeader.vue"; | |
import Sidebar from "../components/Sidebar.vue"; | |
import Feed from "../components/Feed.vue"; | |
import Widget from "../components/Widget.vue"; | |
export default { | |
name: "wall", | |
components: { | |
MainHeader, | |
Sidebar, | |
Feed, | |
Widget, | |
}, | |
}; | |
</script> | |
<style scoped> | |
</style> |
Next, let’s take a look at the various sub-components that supports our application.
The Sub-Components
The following is a list of smaller components, their code snippets, and the roles they play in our app.
- Contact
- Feed
- FriendRequest
- Group
- GroupRequest
- InputBox
- MainHeader
- Message
- Post
- Posts
- Sidebar
- SidebarRow
- Stories
- StoryCard
- User
- Widget
Let’s take a look at the individual functions of these components in detail.
Widget & Contact Component
These components are responsible for rendering a user’s contact list. The widget itself being very responsive embodies all the contacts of the signed-in user. For a better insight into what is happening under the hood, look at the code snippets below.
<template> | |
<div | |
class="flex items-center space-x-3 mb-2 relative hover:bg-gray-200 cursor-pointer p-2 rounded-xl" | |
> | |
<img | |
class="rounded-full h-10 w-10 object-cover" | |
:src="src" | |
loading="lazy" | |
/> | |
<p>{{ name }}</p> | |
<div | |
class="absolute bottom-2 left-7 bg-green-400 h-3 w-3 rounded-full" | |
/> | |
</div> | |
</template> | |
<script> | |
export default { | |
name: "contact", | |
props: ["src", "name"], | |
setup() { | |
return {}; | |
}, | |
}; | |
</script> | |
<style scoped> | |
</style> |
<template> | |
<div class="hidden lg:flex flex-col w-60 p-2 mt-5"> | |
<div class="flex justify-between items-center text-gray-500 mb-5"> | |
<h2 class="text-xl">Contacts</h2> | |
<div class="flex space-x-2"> | |
<VideoCameraIcon class="h-6" /> | |
<SearchIcon class="h-6" /> | |
<DotsHorizontalIcon class="h-6" /> | |
</div> | |
</div> | |
<Contact | |
v-for="(contact, index) in contacts" | |
:key="index" | |
:src="contact.src" | |
:name="contact.name" | |
/> | |
</div> | |
</template> | |
<script> | |
import { SearchIcon } from "@heroicons/vue/outline"; | |
import { DotsHorizontalIcon, VideoCameraIcon } from "@heroicons/vue/solid"; | |
import Contact from "./Contact"; | |
import { ref } from "vue"; | |
export default { | |
name: "widget", | |
setup() { | |
const contacts = ref([ | |
{ src: "https://links.papareact.com/f0p", name: "Jeff Bezoz" }, | |
{ src: "https://links.papareact.com/kxk", name: "Elon Musk" }, | |
{ src: "https://links.papareact.com/zvy", name: "Bill Gates" }, | |
{ src: "https://links.papareact.com/snf", name: "Mark Zuckerberg" }, | |
{ src: "https://links.papareact.com/d0c", name: "Harry Potter" }, | |
{ src: "https://links.papareact.com/6gg", name: "The Queen" }, | |
{ src: "https://links.papareact.com/r57", name: "James Bond" }, | |
]); | |
return { contacts }; | |
}, | |
components: { | |
SearchIcon, | |
DotsHorizontalIcon, | |
VideoCameraIcon, | |
Contact, | |
}, | |
}; | |
</script> | |
<style scoped> | |
</style> |
Please make sure you’re replicating this project in the order as seen in the code previews. Let’s proceed to the next sub-components.
Post & Posts Component
These components are responsible for rendering the carded user posts with or without images. The post component is repeatedly reused within the posts component. The code block below is responsible for producing the above interface.
<template> | |
<div class="flex flex-col"> | |
<div class="p-5 bg-white mt-5 rounded-t-2xl shadow-sm"> | |
<div class="flex items-center space-x-2"> | |
<img class="rounded-full icon" :src="image" width="40" height="40" /> | |
<div> | |
<p class="font-medium">{{ name }}</p> | |
<p v-if="timestamp" class="text-xs text-gray-400"> | |
{{ new Date(timestamp?.toDate()).toLocaleString() }} | |
</p> | |
<p v-else class="text-xs text-gray-400">Loading</p> | |
</div> | |
</div> | |
<p class="pt-4">{{ message }}</p> | |
</div> | |
<div v-if="postImage" class="relative h-56 md:h-96 bg-white"> | |
<img :src="postImage" class="object-cover w-full" loading="lazy" /> | |
</div> | |
<!-- Post Footer --> | |
<div | |
class="flex justify-between items-center rounded-b-2xl bg-white shadow-md text-gray-400 border-t" | |
> | |
<div class="inputIcon p-3 rounded-none rounded-bl-2xl"> | |
<ThumbUpIcon class="h-4" /> | |
<p class="text-xs sm:text-base">Like</p> | |
</div> | |
<div class="inputIcon p-3 rounded-none"> | |
<ChatAltIcon class="h-4" /> | |
<p class="text-xs sm:text-base">Comment</p> | |
</div> | |
<div class="inputIcon p-3 rounded-none rounded-br-2xl"> | |
<ShareIcon class="h-4" /> | |
<p class="text-xs sm:text-base">Share</p> | |
</div> | |
</div> | |
</div> | |
</template> | |
<script> | |
import { ChatAltIcon, ShareIcon, ThumbUpIcon } from "@heroicons/vue/outline"; | |
export default { | |
props: ["name", "message", "email", "timestamp", "image", "postImage"], | |
setup() { | |
return {}; | |
}, | |
components: { ChatAltIcon, ShareIcon, ThumbUpIcon }, | |
}; | |
</script> | |
<style scoped> | |
</style> |
<template> | |
<div> | |
<Post | |
v-for="post in posts" | |
:key="post.id" | |
:name="post.name" | |
:message="post.message" | |
:email="post.email" | |
:timestamp="post.timestamp" | |
:image="post.image" | |
:postImage="post.postImage" | |
/> | |
</div> | |
</template> | |
<script> | |
import db from "../firebase"; | |
import { ref, onMounted } from "vue"; | |
import Post from "./Post.vue"; | |
export default { | |
setup() { | |
const posts = ref([]); | |
const listPosts = () => { | |
db.collection("posts") | |
.orderBy("timestamp", "desc") | |
.onSnapshot((snapshot) => { | |
posts.value = snapshot.docs.map((doc) => { | |
const data = { ...doc.data(), id: doc.id }; | |
return data; | |
}); | |
}); | |
}; | |
onMounted(() => { | |
listPosts(); | |
}); | |
return { posts }; | |
}, | |
components: { | |
Post, | |
}, | |
}; | |
</script> | |
<style scoped> | |
</style> |
Stories & Story Card Components
These components are designed with the responsibility of presenting our Facebook Stories. Using the story card component, the stories component renders the current user’s stories in a responsive approach. Below are the codes responsible for the image above.
<template> | |
<div class="stories flex items-center justify-center space-x-3 mx-auto"> | |
<!-- StoryCard --> | |
<StoryCard | |
v-for="(story, index) in stories" | |
:profile="story.profile" | |
:src="story.src" | |
:name="story.name" | |
:key="index" | |
/> | |
</div> | |
</template> | |
<script> | |
import { ref } from "vue"; | |
import StoryCard from "./StoryCard.vue"; | |
export default { | |
components: { StoryCard }, | |
setup() { | |
const stories = ref([ | |
{ | |
name: "Sonny Sangha", | |
src: "https://links.papareact.com/zof", | |
profile: "https://links.papareact.com/l4v", | |
}, | |
{ | |
name: "Elon Musk", | |
src: "https://links.papareact.com/4zn", | |
profile: "https://links.papareact.com/kxk", | |
}, | |
{ | |
name: "Jeff Bezoz", | |
src: "https://links.papareact.com/k2j", | |
profile: "https://links.papareact.com/f0p", | |
}, | |
{ | |
name: "Mark Zuckerberg", | |
src: "https://links.papareact.com/xql", | |
profile: "https://links.papareact.com/snf", | |
}, | |
{ | |
name: "Bill Gates", | |
src: "https://links.papareact.com/4u4", | |
profile: "https://links.papareact.com/zvy", | |
}, | |
]); | |
return { stories }; | |
}, | |
}; | |
</script> | |
<style scoped> | |
</style> |
<template> | |
<div | |
className="relative h-14 w-14 md:h-20 md:w-20 lg:h-56 lg:w-32 cursor-pointer overflow-x p-3 transition duration-200 transform ease-in hover:scale-105 hover:animate-pulse" | |
> | |
<img | |
:src="profile" | |
:alt="name" | |
class=" | |
h-10 | |
w-10 | |
ml-2 | |
absolute | |
opacity-0 | |
lg:opacity-100 | |
rounded-full | |
z-50 | |
top-6 | |
object-cover | |
" | |
loading="lazy" | |
/> | |
<img | |
:src="src" | |
:alt="name" | |
class=" | |
h-full | |
object-cover | |
filter | |
brightness-75 | |
rounded-full | |
lg:rounded-3xl | |
" | |
loading="lazy" | |
/> | |
<p | |
className="absolute opacity-0 lg:opacity-100 bottom-4 left-5 w-2/3 text-white text-sm font-bold truncate" | |
> | |
{{ name }} | |
</p> | |
</div> | |
</template> | |
<script> | |
export default { | |
name: "story-card", | |
props: ["profile", "name", "src"], | |
setup() { | |
return {}; | |
}, | |
}; | |
</script> | |
<style scoped> | |
</style> |
The Input Box Component
The input box component is responsible for publishing new posts into our platform with or without an image. Behind the scene, the input box component leverages the storage and database services of our Firebase account for all the posts on our application. Below are the codes regulating the post-publishing processes.
<template> | |
<div | |
class="bg-white p-2 rounded-2xl shadow-md text-gray-500 font-medium mt-6" | |
> | |
<div class="flex items-center space-x-4 p-2"> | |
<img | |
:src="user?.photoURL" | |
:alt="user?.displayName" | |
class="h-10 w-10 rounded-full object-cover icon" | |
loading="lazy" | |
/> | |
<form @submit.prevent="onSubmit" class="flex flex-1"> | |
<input | |
type="text" | |
:placeholder="`what's on your mind, ${user?.displayName}?`" | |
class=" | |
rounded-full | |
h-12 | |
bg-gray-100 | |
flex-grow | |
px-5 | |
focus:outline-none | |
" | |
v-model.trim="message" | |
/> | |
</form> | |
</div> | |
<div class="flex justify-evenly p-3 border-t"> | |
<div class="inputIcon"> | |
<VideoCameraIcon class="h-7 text-red-500" /> | |
<p class="text-xs sm:text-sm xl:text-base">Live Video</p> | |
</div> | |
<div @click="$refs.filepickerRef.click()" class="inputIcon"> | |
<CameraIcon class="h-7 text-green-400" /> | |
<p class="text-xs sm:text-sm xl:text-base">Photo/Video</p> | |
<input | |
ref="filepickerRef" | |
type="file" | |
accept="image/png, image/gif, image/jpeg" | |
class="hidden" | |
@change="addImageToPost($event)" | |
/> | |
</div> | |
<div class="inputIcon"> | |
<EmojiHappyIcon class="h-7 text-yellow-300" /> | |
<p class="text-xs sm:text-sm xl:text-base">Feeling/Activity</p> | |
</div> | |
<div | |
class=" | |
flex flex-col | |
filter | |
hover:brightness-110 | |
transition | |
duration-150 | |
transform | |
hover:scale-105 | |
cursor-pointer | |
" | |
v-if="imageToPost" | |
@click="removeImage" | |
> | |
<img :src="imageToPost" class="h-10 object-contain" /> | |
<p class="text-xs text-red-500 text-center">Remove</p> | |
</div> | |
</div> | |
</div> | |
</template> | |
<script> | |
import { ref, onMounted } from "vue"; | |
import { EmojiHappyIcon } from "@heroicons/vue/outline"; | |
import { CameraIcon, VideoCameraIcon } from "@heroicons/vue/solid"; | |
import db, { storage, timestamp } from "../firebase"; | |
export default { | |
setup() { | |
const user = ref(null); | |
const filepickerRef = ref(null); | |
const imageToPost = ref(null); | |
const message = ref(""); | |
onMounted(() => { | |
user.value = JSON.parse(localStorage.getItem("user")); | |
}); | |
const onSubmit = () => { | |
if (message.value === "") return; | |
const data = { | |
message: message.value, | |
name: user.value.displayName, | |
email: user.value.email, | |
image: user.value.photoURL, | |
uid: user.value.uid, | |
timestamp: timestamp, | |
}; | |
db.collection("posts") | |
.add(data) | |
.then((doc) => { | |
if (imageToPost.value) { | |
const uploadTask = storage | |
.ref(`posts/${doc.id}`) | |
.putString(imageToPost.value, "data_url"); | |
removeImage(); | |
uploadTask.on( | |
"state_change", | |
null, | |
(error) => console.log(error), | |
() => { | |
// On a completed upload | |
storage | |
.ref("posts") | |
.child(doc.id) | |
.getDownloadURL() | |
.then((url) => { | |
db.collection("posts").doc(doc.id).set( | |
{ | |
postImage: url, | |
}, | |
{ merge: true } | |
); | |
}); | |
} | |
); | |
} | |
message.value = ""; | |
}) | |
.catch((error) => console.log(error)); | |
}; | |
const addImageToPost = (e) => { | |
const reader = new FileReader(); | |
if (e.target.files[0]) { | |
reader.readAsDataURL(e.target.files[0]); | |
} | |
reader.onload = (readerEvent) => { | |
imageToPost.value = readerEvent.target.result; | |
}; | |
}; | |
const removeImage = () => (imageToPost.value = null); | |
return { | |
user, | |
message, | |
onSubmit, | |
addImageToPost, | |
filepickerRef, | |
imageToPost, | |
removeImage, | |
}; | |
}, | |
components: { | |
EmojiHappyIcon, | |
CameraIcon, | |
VideoCameraIcon, | |
}, | |
}; | |
</script> | |
<style lang="scss" scoped> | |
</style> |
The Feed Component
This component houses all the above-mentioned sub-components which includes the stories, input box, and posts components. These components are all designed together with the tailwind CSS. The codes below contain the component structure and design.
<template> | |
<div class="felx-grow flex-1 h-screen pb-44 pt-6 overflow-y-auto"> | |
<!-- Feeds --> | |
<div class="mx-auto max-w-md md:max-w-lg lg:max-w-2xl"> | |
<!-- Stories --> | |
<Stories /> | |
<!-- Input Box --> | |
<InputBox /> | |
<!-- Posts --> | |
<Posts /> | |
</div> | |
</div> | |
</template> | |
<script> | |
import Posts from "./Posts.vue"; | |
import InputBox from "./InputBox.vue"; | |
import Stories from "./Stories.vue"; | |
export default { | |
components: { | |
Posts, | |
InputBox, | |
Stories, | |
}, | |
}; | |
</script> | |
<style scoped> | |
</style> |
The MainHeader Component
This component is responsible for the navigational structure of our application which includes the essential icons and links such as home, friends, groups, avatar, chat, notification, and so on. Within the block below is the code structure responsible for this component.
<template> | |
<div class="sticky z-58 bg-white flex items-center p-2 lg:px-5 shadow-md"> | |
<!-- Left --> | |
<div class="flex items-center"> | |
<img | |
src="../assets/logo.png" | |
alt="Facebook Logo" | |
width="40" | |
height="40" | |
loading="lazy" | |
class="cursor-pointer" | |
@click="moveTo('/')" | |
/> | |
<div class="flex ml-2 items-center rounded-full bg-gray-100 p-2"> | |
<SearchIcon class="h-6 text-gray-600" /> | |
<input | |
class=" | |
hidden | |
md:inline-flex | |
ml-2 | |
items-center | |
bg-transparent | |
outline-none | |
flex-shrink | |
" | |
type="text" | |
placeholder="Search Facebook" | |
/> | |
</div> | |
</div> | |
<!-- Center --> | |
<div class="flex justify-center flex-grow"> | |
<div class="flex space-x-6 md:space-x-2"> | |
<div | |
class=" | |
flex | |
items-center | |
cursor-pointer | |
md:px-10 | |
sm:h-14 | |
md:hover:bg-gray-100 | |
rounded-xl | |
group | |
" | |
@click="moveTo('/')" | |
> | |
<HomeIcon class="h-5 text-gray-500 group-hover:text-blue-500" /> | |
</div> | |
<div | |
class=" | |
flex | |
items-center | |
cursor-pointer | |
md:px-10 | |
sm:h-14 | |
md:hover:bg-gray-100 | |
rounded-xl | |
group | |
" | |
> | |
<FlagIcon class="h-5 text-gray-500 group-hover:text-blue-500" /> | |
</div> | |
<div | |
class=" | |
flex | |
items-center | |
cursor-pointer | |
md:px-10 | |
sm:h-14 | |
md:hover:bg-gray-100 | |
rounded-xl | |
group | |
" | |
> | |
<PlayIcon class="h-5 text-gray-500 group-hover:text-blue-500" /> | |
</div> | |
<div | |
class=" | |
flex | |
items-center | |
cursor-pointer | |
md:px-10 | |
sm:h-14 | |
md:hover:bg-gray-100 | |
rounded-xl | |
group | |
" | |
@click="moveTo('/users')" | |
> | |
<UsersIcon | |
class="h-5 text-gray-500 group-hover:text-blue-500" | |
/> | |
</div> | |
<div | |
class=" | |
flex | |
items-center | |
cursor-pointer | |
md:px-10 | |
sm:h-14 | |
md:hover:bg-gray-100 | |
rounded-xl | |
group | |
" | |
@click="moveTo('/groups')" | |
> | |
<UserGroupIcon class="h-5 text-gray-500 group-hover:text-blue-500" /> | |
</div> | |
</div> | |
</div> | |
<!-- Right --> | |
<div class="flex items-center sm:space-x-2 justify-end"> | |
<!-- <LogoutIcon class="icon"/> --> | |
<img | |
:src="user?.photoURL" | |
alt="avatar" | |
width="30" | |
height="30" | |
loading="lazy" | |
title="Username" | |
class="icon" | |
/> | |
<p class="whitespace-nowrap font-semibold pr-3 capitalize"> | |
{{ user?.displayName }} | |
</p> | |
<PlusIcon class="icon" /> | |
<ChatIcon class="icon" /> | |
<BellIcon class="icon" /> | |
<ChevronDownIcon class="icon" /> | |
</div> | |
</div> | |
</template> | |
<script> | |
import { | |
BellIcon, | |
ChatIcon, | |
ChevronDownIcon, | |
HomeIcon, | |
UserGroupIcon, | |
UsersIcon, | |
FlagIcon, | |
PlayIcon, | |
SearchIcon, | |
PlusIcon, | |
} from "@heroicons/vue/solid"; | |
import { ref, onMounted } from "vue"; | |
import { useRouter } from "vue-router"; | |
export default { | |
name: "main-header", | |
setup() { | |
let user = ref(null); | |
const router = useRouter(); | |
onMounted(() => { | |
user.value = JSON.parse(localStorage.getItem("user")); | |
}); | |
const moveTo = (path) => { | |
router.push(path); | |
}; | |
return { user, moveTo }; | |
}, | |
components: { | |
BellIcon, | |
ChatIcon, | |
ChevronDownIcon, | |
HomeIcon, | |
UserGroupIcon, | |
PlusIcon, | |
FlagIcon, | |
PlayIcon, | |
SearchIcon, | |
UsersIcon, | |
}, | |
}; | |
</script> | |
<style scoped> | |
</style> |
The Sidebar & Sidebar Row Component
These components are responsible for the rendering of the navigational structures of our application. See the code snippet below for the structure and operations of the components.
The Group Requests Component
The group requests component is responsible for creating and listing the available groups on our platform. Once a group is created with a private or public parameter, our app nicely renders it to the view. Behind the scene, the CometChat SDK is being used here to both create and obtain the list of groups on our platform. Below is the code snippet responsible for this act.
<template> | |
<div class="felx-grow flex-1 h-screen pb-44 pt-6 overflow-y-auto"> | |
<div class="mx-auto max-w-md md:max-w-lg lg:max-w-2xl"> | |
<div | |
class=" | |
bg-white | |
p-2 | |
rounded-2xl | |
shadow-md | |
text-gray-500 | |
font-medium | |
mt-6 | |
" | |
> | |
<div class="flex items-center space-x-4 p-2"> | |
<img | |
:src="user?.photoURL" | |
:alt="user?.displayName" | |
class="h-10 w-10 rounded-full object-cover icon" | |
loading="lazy" | |
/> | |
<form @submit.prevent="onSubmit" class="flex flex-1"> | |
<input | |
type="text" | |
placeholder="Add a new group" | |
class=" | |
rounded-full | |
h-12 | |
bg-gray-100 | |
flex-grow | |
px-5 | |
focus:outline-none | |
" | |
v-model.trim="group.name" | |
/> | |
<select | |
class=" | |
rounded-full | |
h-12 | |
bg-gray-100 | |
flex-grow | |
px-5 | |
focus:outline-none | |
text-base | |
placeholder-gray-600 | |
" | |
placeholder="Select Group Privacy" | |
v-model="group.private" | |
> | |
<option selected disabled hidden value=""> | |
Select Group private | |
</option> | |
<option value="false">Public</option> | |
<option value="true">Private</option> | |
</select> | |
<button | |
type="submit" | |
class=" | |
bg-blue-500 | |
hover:bg-blue-700 | |
text-white | |
font-bold | |
py-2 | |
px-4 | |
rounded-full | |
focus:outline-none | |
" | |
> | |
{{ loading ? "Creating..." : "Create" }} | |
</button> | |
</form> | |
</div> | |
</div> | |
<div | |
class=" | |
flex | |
justify-between | |
bg-white | |
p-2 | |
rounded-2xl | |
shadow-md | |
text-gray-500 | |
font-medium | |
mt-6 | |
" | |
v-for="(group, index) in groups" | |
:key="index" | |
> | |
<div | |
v-if="group?.type === 'public'" | |
class="flex items-center space-x-4 p-2" | |
> | |
<img | |
:src="generateImageFromIntial(group?.name)" | |
:alt="group?.name" | |
class="h-10 w-10 rounded-full object-cover" | |
loading="lazy" | |
/> | |
<p>{{ group?.name }}</p> | |
</div> | |
<div v-else class="flex items-center space-x-4 p-2"> | |
<img | |
:src="generateImageFromIntial(group?.name)" | |
:alt="group?.name" | |
class="h-10 w-10 rounded-full object-cover" | |
loading="lazy" | |
/> | |
<p>{{ group?.name }}</p> | |
</div> | |
<div v-if="!group?.hasJoined" class="flex items-center space-x-4 p-2"> | |
<button | |
class=" | |
bg-blue-500 | |
hover:bg-blue-700 | |
text-white | |
font-bold | |
py-2 | |
px-4 | |
rounded-full | |
focus:outline-none | |
" | |
@click="joinGroup(group?.guid)" | |
v-if="group?.type === 'public'" | |
> | |
Join Group | |
</button> | |
<button | |
class=" | |
bg-blue-500 | |
text-white | |
font-bold | |
py-2 | |
px-4 | |
rounded-full | |
italic | |
hover:bg-blue-700 | |
focus:outline-none | |
disabled:opacity-50 | |
" | |
disabled | |
v-else | |
> | |
Private | |
</button> | |
</div> | |
<div v-else class="flex items-center space-x-4 p-2"> | |
<button | |
class=" | |
bg-blue-500 | |
hover:bg-blue-700 | |
text-white | |
font-bold | |
py-2 | |
px-4 | |
rounded-full | |
focus:outline-none | |
" | |
@click="moveTo(`/chats/group/${group?.guid}`)" | |
> | |
Enter Group | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</template> | |
<script> | |
import { ref, onMounted } from "vue"; | |
import { useRouter } from "vue-router"; | |
import { CometChat } from "@cometchat-pro/chat"; | |
export default { | |
setup() { | |
const router = useRouter(); | |
const user = ref(null); | |
const loading = ref(false); | |
const group = ref({ name: "", private: "" }); | |
const groups = ref([]); | |
const getGroups = () => { | |
const limit = 30; | |
const groupsRequest = new CometChat.GroupsRequestBuilder() | |
.setLimit(limit) | |
.build(); | |
groupsRequest | |
.fetchNext() | |
.then((groupList) => groups.value = groupList) | |
.catch((error) => { | |
console.log("Groups list fetching failed with error", error); | |
}); | |
}; | |
const joinGroup = (GUID) => { | |
const password = ""; | |
const groupType = CometChat.GROUP_TYPE.PUBLIC; | |
CometChat.joinGroup(GUID, groupType, password) | |
.then((group) => { | |
const index = groups.value.findIndex(g => g.guid === GUID) | |
groups.value[index] = group | |
console.log("Group joined successfully:", group); | |
alert("Group joined successfully"); | |
}) | |
.catch((error) => { | |
console.log("Group joining failed with exception:", error); | |
alert(error.message); | |
}); | |
}; | |
const moveTo = (path) => { | |
router.push(path); | |
}; | |
const onSubmit = () => { | |
if (group.value.name === "" || group.value.private === "") return; | |
loading.value = true; | |
const GUID = generateGUID(); | |
const groupName = group.value.name; | |
const groupType = | |
group.value.private === "true" | |
? CometChat.GROUP_TYPE.PRIVATE | |
: CometChat.GROUP_TYPE.PUBLIC; | |
const password = ""; | |
const Group = new CometChat.Group(GUID, groupName, groupType, password); | |
CometChat.createGroup(Group) | |
.then((res) => { | |
groups.value.unshift(res); | |
console.log("Group created successfully:", res); | |
group.value.name = ""; | |
group.value.private = ""; | |
loading.value = false; | |
}) | |
.catch((error) => { | |
console.log("Group creation failed with exception:", error); | |
loading.value = false; | |
alert(error.message); | |
}); | |
}; | |
const generateGUID = (length = 20) => { | |
const result = []; | |
const characters = | |
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | |
const charactersLength = characters.length; | |
for (let i = 0; i < length; i++) { | |
result.push( | |
characters.charAt(Math.floor(Math.random() * charactersLength)) | |
); | |
} | |
return result.join(""); | |
}; | |
const generateImageFromIntial = (name) => { | |
let canvas = document.createElement("canvas"); | |
canvas.style.display = "none"; | |
canvas.width = "32"; | |
canvas.height = "32"; | |
document.body.appendChild(canvas); | |
let context = canvas.getContext("2d"); | |
context.fillStyle = "#999"; | |
context.fillRect(0, 0, canvas.width, canvas.height); | |
context.font = "16px Arial"; | |
context.fillStyle = "#ccc"; | |
if (name && name != "") { | |
let initials = name[0]; | |
context.fillText(initials.toUpperCase(), 10, 23); | |
let data = canvas.toDataURL(); | |
document.body.removeChild(canvas); | |
return data; | |
} else { | |
return false; | |
} | |
}; | |
onMounted(() => { | |
user.value = JSON.parse(localStorage.getItem("user")); | |
getGroups(); | |
}); | |
return { | |
loading, | |
user, | |
group, | |
groups, | |
onSubmit, | |
joinGroup, | |
generateImageFromIntial, | |
moveTo, | |
}; | |
}, | |
}; | |
</script> | |
<style scoped> | |
</style> |
The Friend Requests Component
Similar to the mode of operation of the group request component, the friend request component lists the available users on our platform. Also, the component gives a user the chance to send a friend request to other users on our platform. The code snippet below illustrates this better.
<template> | |
<div class="felx-grow flex-1 h-screen pb-44 pt-6 overflow-y-auto"> | |
<div class="mx-auto max-w-md md:max-w-lg lg:max-w-2xl"> | |
<div | |
class=" | |
flex | |
justify-between | |
bg-white | |
p-2 | |
rounded-2xl | |
shadow-md | |
text-gray-500 | |
font-medium | |
mt-6 | |
" | |
v-for="(user, index) in users" | |
:key="index" | |
> | |
<router-link | |
:to="`/chats/user/${user?.uid}`" | |
class="flex items-center space-x-4 p-2" | |
> | |
<img | |
:src="user?.avatar" | |
:alt="user?.name" | |
class="h-10 w-10 rounded-full object-cover" | |
loading="lazy" | |
/> | |
<p>{{ user?.name }}</p> | |
</router-link> | |
<div class="flex items-center space-x-4 p-2"> | |
<button | |
class=" | |
bg-blue-500 | |
hover:bg-blue-700 | |
text-white | |
font-bold | |
py-2 | |
px-4 | |
rounded-full | |
focus:outline-none | |
" | |
@click="addFriend(user?.uid)" | |
> | |
Add as Friend | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</template> | |
<script> | |
import { ref, onMounted } from "vue"; | |
import { CometChat } from "@cometchat-pro/chat"; | |
import { cometChat } from "../app.config"; | |
export default { | |
setup() { | |
const user = ref(null); | |
const users = ref([]); | |
const getUsers = () => { | |
const limit = 20; | |
const usersRequest = new CometChat.UsersRequestBuilder() | |
.setLimit(limit) | |
.build(); | |
usersRequest | |
.fetchNext() | |
.then((userList) => (users.value = userList)) | |
.catch((error) => { | |
console.log("User list fetching failed with error:", error); | |
}); | |
}; | |
const addFriend = (uid) => { | |
const url = `https://api-us.cometchat.io/v2.0/users/${user.value.uid}/friends`; | |
const options = { | |
method: "POST", | |
headers: { | |
Accept: "application/json", | |
"Content-Type": "application/json", | |
appId: cometChat.APP_ID, | |
apiKey: cometChat.REST_KEY, | |
}, | |
body: JSON.stringify({ accepted: [uid] }), | |
}; | |
fetch(url, options) | |
.then((res) => { | |
console.log(res); | |
alert("Added as friend successfully"); | |
}) | |
.catch((err) => console.error("error:" + err)); | |
}; | |
onMounted(() => { | |
user.value = JSON.parse(localStorage.getItem("user")); | |
getUsers(); | |
}); | |
return { user, users, addFriend }; | |
}, | |
}; | |
</script> | |
<style scoped> | |
</style> |
The Group, User, & Message Components
These components are responsible for the chatting functionality of our application. The group component caters for group chat whereas, the user component allows for a one-on-one chat. The message component is a reusable component responsible for rendering messages in the chat interface. Below are the code blocks responsible for their operations.
<template> | |
<div class="felx-grow flex-1 h-screen pb-44 pt-6"> | |
<div | |
id="messages" | |
class="mx-auto max-w-md md:max-w-lg lg:max-w-2xl overflow-y-auto overflow-x-hidden" | |
> | |
<div v-for="(msg, index) in messages" :key="index" class="message"> | |
<Message | |
:name="msg?.receiverId !== group?.guid ? group?.name : msg.sender.name" | |
:avatar=" | |
msg?.receiverId !== group?.guid ? group?.avatar : msg.sender.avatar | |
" | |
:message="msg.text" | |
:timestamp="msg.sentAt" | |
:isRight="msg?.sender?.uid !== user?.uid.toLowerCase()" | |
/> | |
</div> | |
</div> | |
<div class="border-t-2 border-gray-200 px-4 pt-4 mb-2 sm:mb-0"> | |
<form @submit.prevent="sendMessage" class="relative flex"> | |
<input | |
type="text" | |
class=" | |
w-full | |
focus:outline-none | |
focus:placeholder-gray-400 | |
text-gray-600 | |
placeholder-gray-600 | |
pl-12 | |
bg-gray-200 | |
rounded-full | |
py-3 | |
" | |
:placeholder="`Message ${group?.name}`" | |
v-model.trim="message" | |
/> | |
<div class="absolute right-0 items-center inset-y-0 hidden sm:flex"> | |
<button | |
type="button" | |
class=" | |
inline-flex | |
items-center | |
justify-center | |
rounded-full | |
h-12 | |
w-12 | |
transition | |
duration-500 | |
ease-in-out | |
text-white | |
bg-blue-500 | |
hover:bg-blue-400 | |
focus:outline-none | |
" | |
> | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
viewBox="0 0 20 20" | |
fill="currentColor" | |
class="h-6 w-6 transform rotate-90" | |
> | |
<path | |
d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" | |
></path> | |
</svg> | |
</button> | |
</div> | |
</form> | |
</div> | |
</div> | |
</template> | |
<script> | |
import { ref, onBeforeMount, onUpdated } from "vue"; | |
import { CometChat } from "@cometchat-pro/chat"; | |
import Message from "./Message.vue"; | |
export default { | |
props: ["guid"], | |
components: { Message }, | |
setup(props) { | |
const user = ref(null); | |
const group = ref(null); | |
const messages = ref([]); | |
const message = ref(""); | |
onBeforeMount(() => { | |
getGroup(props.guid); | |
getMessages(props.guid); | |
listenForMessage(props.guid); | |
user.value = JSON.parse(localStorage.getItem('user')) | |
}); | |
onUpdated(() => scrollToEnd()); | |
const listenForMessage = (listenerID) => { | |
CometChat.addMessageListener( | |
listenerID, | |
new CometChat.MessageListener({ | |
onTextMessageReceived: (message) => { | |
messages.value.push(message); | |
scrollToEnd(); | |
}, | |
}) | |
); | |
}; | |
const getMessages = (guid) => { | |
const limit = 50; | |
const messagesRequest = new CometChat.MessagesRequestBuilder() | |
.setLimit(limit) | |
.setGUID(guid) | |
.build(); | |
messagesRequest | |
.fetchPrevious() | |
.then((msgs) => { | |
messages.value = msgs.filter( | |
(m) => m.type === "text" && typeof m.text != "undefined" | |
); | |
scrollToEnd(); | |
}) | |
.catch((error) => | |
console.log("Message fetching failed with error:", error) | |
); | |
}; | |
const sendMessage = () => { | |
const receiverID = props.guid; | |
const messageText = message.value; | |
const receiverType = CometChat.RECEIVER_TYPE.GROUP; | |
const textMessage = new CometChat.TextMessage( | |
receiverID, | |
messageText, | |
receiverType | |
); | |
CometChat.sendMessage(textMessage) | |
.then((msg) => { | |
messages.value.push(msg); | |
message.value = ""; | |
scrollToEnd(); | |
}) | |
.catch((error) => | |
console.log("Message sending failed with error:", error) | |
); | |
}; | |
const getGroup = (guid) => { | |
CometChat.getGroup(guid) | |
.then((g) => (group.value = g)) | |
.catch((error) => { | |
console.log("User details fetching failed with error:", error); | |
}); | |
}; | |
const scrollToEnd = () => { | |
const elmnt = document.getElementById("messages"); | |
elmnt.scrollTop = elmnt.scrollHeight; | |
}; | |
return { user, group, message, messages, sendMessage }; | |
}, | |
}; | |
</script> | |
<style scoped> | |
#messages { | |
height: calc(100vh - 200px); | |
} | |
</style> |
<template> | |
<div class="mb-4"> | |
<div v-if="isRight" class="flex items-end justify-start"> | |
<img | |
class=" | |
h-10 | |
w-10 | |
rounded-full | |
object-cover | |
xl:inline-flex | |
p-2 | |
h-10 | |
w-10 | |
bg-gray-200 | |
rounded-full | |
text-gray-700 | |
cursor-pointer | |
hover:bg-gray-300 | |
" | |
loading="lazy" | |
:src="avatar" | |
:alt="name" | |
/> | |
<div class="w-2/4 bg-white p-5 rounded-r-2xl rounded-t-2xl ml-2"> | |
<p>{{ message }}</p> | |
<small class="font-medium">{{ new Date(1000 * timestamp).toLocaleString() }}</small> | |
</div> | |
</div> | |
<div v-else class="flex items-end justify-end"> | |
<div class="w-2/4 bg-blue-100 p-5 rounded-l-2xl rounded-t-2xl mr-2"> | |
<p>{{ message }}</p> | |
<small class="font-medium">{{ new Date(1000 * timestamp).toLocaleString() }}</small> | |
</div> | |
<img | |
class=" | |
h-10 | |
w-10 | |
rounded-full | |
object-cover | |
xl:inline-flex | |
p-2 | |
h-10 | |
w-10 | |
bg-gray-200 | |
rounded-full | |
text-gray-700 | |
cursor-pointer | |
hover:bg-gray-300 | |
" | |
loading="lazy" | |
:src="avatar" | |
:alt="name" | |
/> | |
</div> | |
</div> | |
</template> | |
<script> | |
export default { | |
props: { | |
name: { type: String }, | |
avatar: { type: String }, | |
message: { type: String }, | |
timestamp: { type: Number }, | |
isRight: { type: Boolean, default: false }, | |
}, | |
setup() { | |
return {}; | |
}, | |
}; | |
</script> | |
<style scoped> | |
</style> |
<template> | |
<div class="felx-grow flex-1 h-screen pb-44 pt-6"> | |
<div | |
id="messages" | |
class="mx-auto max-w-md md:max-w-lg lg:max-w-2xl overflow-y-auto overflow-x-hidden" | |
> | |
<div v-for="(msg, index) in messages" :key="index" class="message"> | |
<Message | |
:name="msg?.receiverId !== user?.uid ? user?.name : msg.sender.name" | |
:avatar=" | |
msg?.receiverId !== user?.uid ? user?.avatar : msg.sender.avatar | |
" | |
:message="msg.text" | |
:timestamp="msg.sentAt" | |
:isRight="msg?.receiverId !== user?.uid" | |
/> | |
</div> | |
</div> | |
<div class="border-t-2 border-gray-200 px-4 pt-4 mb-2 sm:mb-0"> | |
<form @submit.prevent="sendMessage" class="relative flex"> | |
<input | |
type="text" | |
class=" | |
w-full | |
focus:outline-none | |
focus:placeholder-gray-400 | |
text-gray-600 | |
placeholder-gray-600 | |
pl-12 | |
bg-gray-200 | |
rounded-full | |
py-3 | |
" | |
:placeholder="`Message ${user?.name}`" | |
v-model.trim="message" | |
/> | |
<div class="absolute right-0 items-center inset-y-0 hidden sm:flex"> | |
<button | |
type="button" | |
class=" | |
inline-flex | |
items-center | |
justify-center | |
rounded-full | |
h-12 | |
w-12 | |
transition | |
duration-500 | |
ease-in-out | |
text-white | |
bg-blue-500 | |
hover:bg-blue-400 | |
focus:outline-none | |
" | |
> | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
viewBox="0 0 20 20" | |
fill="currentColor" | |
class="h-6 w-6 transform rotate-90" | |
> | |
<path | |
d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" | |
></path> | |
</svg> | |
</button> | |
</div> | |
</form> | |
</div> | |
</div> | |
</template> | |
<script> | |
import { ref, onBeforeMount, onUpdated } from "vue"; | |
import { CometChat } from "@cometchat-pro/chat"; | |
import Message from "./Message.vue"; | |
export default { | |
props: ["uid"], | |
components: { Message }, | |
setup(props) { | |
const user = ref(null); | |
const messages = ref([]); | |
const message = ref(""); | |
onBeforeMount(() => { | |
getUser(props.uid); | |
getMessages(props.uid); | |
listenForMessage(props.uid); | |
}); | |
onUpdated(() => scrollToEnd()); | |
const listenForMessage = (listenerID) => { | |
CometChat.addMessageListener( | |
listenerID, | |
new CometChat.MessageListener({ | |
onTextMessageReceived: (message) => { | |
messages.value.push(message); | |
scrollToEnd(); | |
}, | |
}) | |
); | |
}; | |
const getMessages = (uid) => { | |
const limit = 50; | |
const messagesRequest = new CometChat.MessagesRequestBuilder() | |
.setLimit(limit) | |
.setUID(uid) | |
.build(); | |
messagesRequest | |
.fetchPrevious() | |
.then((msgs) => { | |
messages.value = msgs.filter( | |
(m) => m.type === "text" && typeof m.text != "undefined" | |
); | |
scrollToEnd(); | |
}) | |
.catch((error) => | |
console.log("Message fetching failed with error:", error) | |
); | |
}; | |
const sendMessage = () => { | |
const receiverID = props.uid; | |
const messageText = message.value; | |
const receiverType = CometChat.RECEIVER_TYPE.USER; | |
const textMessage = new CometChat.TextMessage( | |
receiverID, | |
messageText, | |
receiverType | |
); | |
CometChat.sendMessage(textMessage) | |
.then((msg) => { | |
messages.value.push(msg); | |
message.value = ""; | |
scrollToEnd(); | |
}) | |
.catch((error) => | |
console.log("Message sending failed with error:", error) | |
); | |
}; | |
const getUser = (uid) => { | |
CometChat.getUser(uid) | |
.then((u) => (user.value = u)) | |
.catch((error) => { | |
console.log("User details fetching failed with error:", error); | |
}); | |
}; | |
const scrollToEnd = () => { | |
const elmnt = document.getElementById("messages"); | |
elmnt.scrollTop = elmnt.scrollHeight; | |
}; | |
return { user, message, messages, sendMessage }; | |
}, | |
}; | |
</script> | |
<style scoped> | |
#messages { | |
height: calc(100vh - 200px); | |
} | |
</style> |
The Users View
The Users View is responsible for displaying the number of users within our platform along with a nice “Add as a Friend” blue button. It also navigates the user to the chat view.
Go through the code below to examine how all the components are put together and married by the tailwind CSS.
<template> | |
<div class="friends"> | |
<MainHeader /> | |
<main class="flex"> | |
<Sidebar /> | |
<FriendRequests /> | |
<Widget /> | |
</main> | |
</div> | |
</template> | |
<script> | |
import MainHeader from "../components/MainHeader.vue"; | |
import Sidebar from "../components/Sidebar.vue"; | |
import FriendRequests from "../components/FriendRequests.vue"; | |
import Widget from "../components/Widget.vue"; | |
export default { | |
name: "friends", | |
components: { | |
MainHeader, | |
Sidebar, | |
FriendRequests, | |
Widget, | |
}, | |
}; | |
</script> | |
<style scoped> | |
</style> |
The Groups View
Almost like the Users view, the Groups component is responsible for creating and rendering out all the available groups in our platform. Also, it ensures that private groups are hidden from other user's accessibility. Observe the image and code below.
<template> | |
<div class="friends"> | |
<MainHeader /> | |
<main class="flex"> | |
<Sidebar /> | |
<GroupRequests /> | |
<Widget /> | |
</main> | |
</div> | |
</template> | |
<script> | |
import MainHeader from "../components/MainHeader.vue"; | |
import Sidebar from "../components/Sidebar.vue"; | |
import GroupRequests from "../components/GroupRequests.vue"; | |
import Widget from "../components/Widget.vue"; | |
export default { | |
name: "friends", | |
components: { | |
MainHeader, | |
Sidebar, | |
GroupRequests, | |
Widget, | |
}, | |
}; | |
</script> | |
<style scoped> | |
</style> |
The Chats View
This view is responsible for adequately displaying either a group chat or a one-on-one chat depending on the kind of chat selected by the user. This messaging ability of this component is sponsored by the CometChat SDK.

The codes below explains it all. Please ensure that you go through all the folders in the project to see how they all merge, especially the components directory.
<template> | |
<div class="chats"> | |
<MainHeader /> | |
<main class="flex"> | |
<Sidebar /> | |
<User v-if="type === 'user'" :uid="id" /> | |
<Group v-else :guid="id" /> | |
<Widget /> | |
</main> | |
</div> | |
</template> | |
<script> | |
import MainHeader from "../components/MainHeader.vue"; | |
import Sidebar from "../components/Sidebar.vue"; | |
import User from "../components/User.vue"; | |
import Group from "../components/Group.vue"; | |
import Widget from "../components/Widget.vue"; | |
export default { | |
name: "chats", | |
props: ['id', 'type'], | |
setup() { | |
return {}; | |
}, | |
components: { | |
MainHeader, | |
Sidebar, | |
User, | |
Group, | |
Widget, | |
}, | |
}; | |
</script> | |
<style scoped> | |
</style> |
Way to go, you’ve just crushed it, a powerful and working clone of Facebook. Their is one more thing to do, start up your server if you haven’t done that already using the command below.
npm run serve
Conclusion
In conclusion, we have done an amazing job in developing a Facebook clone by leveraging VueJs, Firebase, CometChat, and Tailwind CSS. You’ve been introduced to the chemistry behind Facebook and how the CometChat SDK makes social networking applications buildable.
You have seen how to integrate most of the CometChat functionalities such as texting and real-time messaging. I hope you enjoyed this tutorial and that you were able to successfully clone Facebook.
It's time to get busy and build other related applications with the skills you have gotten from this tutorial.
About Author
Gospel Darlington is a remote full-stack web developer, prolific in Frontend and API development. He takes a huge interest in the development of high-grade and responsive web applications. He is currently exploring new techniques for improving progressive web applications (PWA). Gospel Darlington currently works as a freelancer and spends his free time coaching young people on how to become successful in life. His hobbies include inventing new recipes, book writing, songwriting, and singing. You can reach me on LinkedIn, Twitter, Facebook, or GitHub.
Top comments (0)