35
How To Build Your Own Likes and Comments System With Firebase and React
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.
npm i firebase
// 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: '',
authDomain: '',
projectId: '',
storageBucket: '',
messagingSenderId: '',
appId: '',
}
if (!firebase.apps.length) {
firebase.initializeApp(firebaseConfig)
} else {
firebase.app()
}
export const firestore = firebase.firestore()
export default firebase
// File: components/blog/like.js
import { firestore } from '@/lib/firebase'
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)
})
}
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)
})
}
// File: components/blog/comments.js
import { useState } from 'react'
import firebase, { firestore } from '@/lib/firebase'
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)
})
}
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)
})
}
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>
))
}
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
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} />
</>
}
35