One of my todo items with React apps was having performance-first dynamic comments and likes system for static websites. Why? Because it brings the capabilities of going beyond content and add functionalities which invite user engagement.
Both Cusdis and Disqus are not performance-friendly as they highly affect Cumulative Layout Shift (CLS).
So I set out on creating the system with Firebase, TailwindCSS and React. While TailwindCSS is not a compulsion, it's my go to library. Let's get started
Setting Up Firebase
- Install Firebase (Client Side) with the following command:
# Using NPM
npm install firebase
# Using YARN
yarn add firebase
- Create firebase.js with the following configuration:
// File: @/lib/firebase.js
import 'firebase/firestore'
import firebase from 'firebase/app'
// More about firebase config on https://firebase.google.com/docs/web/setup#config-object
var firebaseConfig = {
apiKey: process.env.API_KEY,
authDomain: process.env.AUTH_DOMAIN,
projectId: process.env.PROJECT_ID,
storageBucket: process.env.STORAGE_BUCKET,
messagingSenderId: process.env.MESSAGING_SENDER_ID,
appId: process.env.APP_ID,
}
if (!firebase.apps.length) {
firebase.initializeApp(firebaseConfig)
} else {
firebase.app()
}
export const firestore = firebase.firestore()
export default firebase
Creating Like Component
- Create the like.js file:
// File: @/components/blog/like.js
import { firestore } from '@/lib/firebase'
- Add the getLikes function which takes in the slug of the blog page, and a callback function if needed.
export const getLikes = (slug, callBackFunction) => {
firestore
.collection('likes')
.doc(slug)
.get()
.then((doc) => {
if (doc.exists) {
callBackFunction(Object.keys(doc.data()).length)
}
})
.catch((err) => {
console.error(err)
})
}
- Add the postLike function which takes in the slug of the blog page, and a callback function if needed.
export const postLike = (slug, callBackFunction) => {
fetch('https://api.ipify.org/?format=json', {
method: 'GET',
})
.then((res) => res.json())
.then((res) => {
firestore
.collection('likes')
.doc(slug)
.set(
{
[res['ip']]: null,
},
{ merge: true }
)
.then(callBackFunction)
})
.catch((err) => {
console.error(err)
})
}
Creating Comment Component
- Create the comment.js file:
// File: @/components/blog/comments.js
import { useState } from 'react'
import firebase, { firestore } from '@/lib/firebase'
- Adding the getComments function which takes in the slug of the blog page, and a callback function if needed.
export const getComments = (slug, callBackFunction) => {
firestore
.collection('comments')
.get()
.then((snapshot) => {
const posts = snapshot.docs
.map((doc) => doc.data())
.filter((doc) => doc.slug === slug)
.map((doc) => {
return { id: doc.id, ...doc }
})
callBackFunction(posts)
})
.catch((err) => {
console.log(err)
})
}
- Adding the writeComment function which takes in the slug of the blog page, and a callback function if needed.
export const writeComment = (name, slug, content, email, callBackFunction) => {
let temp = {
name,
slug,
content,
time: firebase.firestore.Timestamp.fromDate(new Date()),
}
if (email.length > 0) temp['email'] = email
firestore
.collection('comments')
.add(temp)
.then(() => {
callBackFunction()
})
.catch((err) => {
console.error(err)
})
}
- Creating the LoadComments function which takes in the set of the comments to display
export const LoadComments = ({ comments }) => {
return comments
.sort((a, b) =>
a.time.toDate().getTime() > b.time.toDate().getTime() ? -1 : 1
)
.map((item) => (
<div
key={item.time.seconds}
className="border dark:border-gray-500 rounded p-5 w-full mt-5 flex flex-col"
>
<span className="text-lg text-gray-500 dark:text-gray-300 font-medium">
{item.name} · {item.time.toDate().toDateString()}
</span>
<span className="mt-3 text-md text-gray-500 dark:text-gray-300">
{item.content}
</span>
</div>
))
}
- Creating the WriteComment component which takes in the slug of the blog page, and setComments for setting the new set of comments to be displayed.
const WriteComment = ({ slug, setComments }) => {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [comment, setComment] = useState('')
return (
<form
onSubmit={(e) => {
e.preventDefault()
writeComment(name, slug, comment, email, () =>
getComments(slug, setComments)
)
setName('')
setEmail('')
setComment('')
}}
className="mt-10 flex flex-col w-full"
>
<h1 className="font-semibold text-lg">Write a comment</h1>
<div className="flex flex-col sm:flex-row sm:space-x-5 items-start">
<input
required
value={name}
placeholder="Name*"
onChange={(e) => setName(e.target.value)}
className="mt-5 w-full sm:w-1/2 appearance-none outline-none ring-0 px-5 py-2 border dark:hover:border-white hover:border-black rounded hover:shadow text-black dark:bg-black dark:text-gray-300 dark:border-gray-500"
/>
<div className="mt-5 w-full sm:w-1/2 flex flex-col space-y-1">
<input
value={email}
placeholder="Email (Optional)"
onChange={(e) => setEmail(e.target.value)}
className="w-full appearance-none outline-none ring-0 px-5 py-2 border dark:hover:border-white hover:border-black rounded hover:shadow text-black dark:bg-black dark:text-gray-300 dark:border-gray-500"
/>
<span className="text-sm text-gray-400">
Email will remain confidential.
</span>
</div>
</div>
<textarea
required
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder={'Comment*\nMaximum of 500 characters.'}
className="mt-5 appearance-none outline-none ring-0 pt-5 px-5 pb-10 border dark:hover:border-white hover:border-black rounded hover:shadow text-black dark:bg-black dark:text-gray-300 dark:border-gray-500"
/>
<button
type="submit"
className="w-[200px] appearance-none mt-5 py-2 px-5 text-center rounded border hover:bg-gray-100 dark:hover:bg-[#28282B] dark:border-gray-500"
>
Post a comment
</button>
</form>
)
}
export default WriteComment
Creating Dynamic Blog Component
Load the components in the dynamic blog [slug].js file:
import WriteComment, { getComments, LoadComments } from '@/components/blog/comments'
export default function Post({ post }) {
const [comments, setComments] = useState([])
return <>
<WriteComment setComments={setComments} slug={post.slug} />
<div className="mt-10 pt-10 w-full border-t dark:border-gray-500">
<button
onClick={() => getComments(post.slug, setComments)}
className="w-[200px] appearance-none py-2 px-5 text-center rounded border hover:bg-gray-100 dark:hover:bg-[#28282B] dark:border-gray-500"
>
Load Comments
</button>
</div>
<LoadComments comments={comments} />
</>
}
Example
Well, just below this line you see the components in action! The source code is available here.