Adding a comment box with google firebase
Table of Contents
1. Why
I never played enough with firebase and felt like a good opportunity. The final result is far from perfect, without optimizations for loading too many comments, spam prevention and dynamically loading.
I had the idea after watching this unfinished series on youtube.
2. Code
2.1. Html
Nothing really special here.
<div id="comments"> <h2>Comments</h2> <form id="new-comment-form"> <label for="username">Post a new comment</label><br> <input type="text" id="comment-name" placeholder="Your name" minlength="4" maxlength="48" required> <textarea id="comment-box" placeholder="Your comment here" minlength="10" maxlength="512" rows="5" style="resize: none;" required></textarea> <button type="submit" id="post-comment">Post</button> </form> <div id="comments-section"> <h4 id="nocomment">Be the first to comment!</h4> <ul id="comment-list"> </ul> </div> </div> <script type="module" src="/comments.js"></script>
2.2. Css
This could be improved, hire a designer? I wont.
#comments { color-scheme: dark; margin: 10px; display: none; } #new-comment-form { padding: 5px; } #comment-list { padding: 5px; list-style: none; } #comment-box, #post { border: none; border-radius: 5px; boder-color: #3c81ba; margin: 10px 0 6px 0; } #comments-section { margin-top: 10%; overflow-y: auto; max-height: 500px; } #post-comment { float: right; margin-right: 5%; border-radius: 20%; } #post-comment:hover{ background-color: #a20a29; } div.comment { background-color: #212121; border-radius: 15px; margin-top: 20px; padding-left: 10px; padding-bottom: 5px; } div.comment span { color: #5ea6e1; } div.comment p { text-align: center; text-justify: inter-word; margin: 5px 12px; background: #373636; border: #3c81ba; border-width: 2px; border-radius: 5px; font-size: small; border-style: solid; } div.comment timestamp { float: right; padding-right: 50%; font-size: small; }
2.3. Javascript
Now here the magic happens. Ignore the cookie stuff. We initialize a firebase realtime database client as firebase will recommend us itself, choosing the cdn route.
// Import the functions you need from the SDKs you need import { initializeApp } from "https://www.gstatic.com/firebasejs/9.6.11/firebase-app.js"; import { getDatabase, ref, set, get, child } from "https://www.gstatic.com/firebasejs/9.6.11/firebase-database.js"; const setupComments = () => { if (!isOnPost) { return } const firebaseConfig = { apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", authDomain: "blog-comments-xxxxx.firebaseapp.com", databaseURL: "https://blog-comments-xxxxx-default-rtdb.firebaseio.com", projectId: "blog-comments-xxxxx", storageBucket: "blog-comments-xxxxx.appspot.com", messagingSenderId: "xxxxxxxxxxxx", appId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", measurementId: "G-xxxxxxxxxx" }; // Initialize Firebase const app = initializeApp(firebaseConfig); // Get a reference to the database service const database = getDatabase(app); // Page path const path = window.location.pathname.toString().split("/").slice(0, -1).join("/") String.prototype.hash = function() { var hash = 0; for (var i = 0; i < this.length; i++) { var code = this.charCodeAt(i); hash = ((hash << 5) - hash) + code; hash = hash & hash; // Convert to 32bit integer } return Math.abs(hash).toString(); } // DB functions async function postComment(name, text) { const time = Date.now() const fullpath = path.hash() + "-" + path.split("/").slice(-1) + "/" + name.hash() + time set(ref(database, fullpath), { name, text, time: time, }); } function checkType(obj, type) { return obj && typeof (obj) === type } // Fill with comments const fullpath = path.hash() + "-" + path.split("/").slice(-1) get(child(ref(database), fullpath)).then((snapshot) => { if (snapshot.exists()) { const comments = snapshot.val(); for (const cid of Object.keys(comments)) { const comment = comments[cid] if (checkType(comment.name, "string") && checkType(comment.text, "string") && checkType(comment.time, "number")) addCommentHtml(comment) } } document.getElementById("comments").style.display = "block" }).catch((error) => { console.error(error); document.getElementById("comments").style.display = "block" }) function addCommentHtml(commentObj) { document.getElementById("nocomment").style.display = "none" var li = document.createElement("li"); const div = document.createElement("div") div.classList.add("comment") const name = document.createTextNode(commentObj.name); const nameSpan = document.createElement("span") nameSpan.append(name) const comment = document.createTextNode(commentObj.text); const commentp = document.createElement("p") commentp.append(comment) const utctime = commentObj.time const timestamp_text = "in " + new Date(utctime).toLocaleDateString() + " " + new Date(utctime).toLocaleTimeString() const time = document.createTextNode(timestamp_text); const timestamp = document.createElement("timestamp") timestamp.append(time) div.appendChild(nameSpan); div.appendChild(timestamp); div.appendChild(commentp); li.appendChild(div) document.getElementById("comment-list").prepend(li); } function processUserCommentAdd() { const name = document.getElementById("comment-name"); const text = document.getElementById("comment-box"); addCommentHtml({ name: name.value, text: text.value, time: Date.now() }) postComment(name.value, text.value) setCookie('username', name.value); name.disabled = true text.value = "" } const form = document.getElementById("new-comment-form"); form.addEventListener("submit", function(e) { e.preventDefault() processUserCommentAdd() }); document.body.addEventListener('keydown', function(e) { if (!(e.key === 'Enter' && (e.metaKey || e.ctrlKey))) return; const target = e.target if (target.form) { e.preventDefault() if (form.checkValidity() === false) { form.reportValidity(); return; } processUserCommentAdd() } }) } setupComments();
2.4. Firebase rules
Basically only allow adding new content inside a /cat_id/post_id/
, not updating or deleting allowed. The data must contain the fields name
, text
and time
.
Then some type checking is applied and it is ensured that the timestamp is not from the future or older than 15 seconds.
{ "rules": { "$category": { ".write": false, ".read": true, "$postid": { ".read": true, ".write": "!data.exists()", ".validate": "$category.matches(/^\\d+-\\w+$/)", "name": {".validate": "newData.isString() && newData.val().length <= 48 && newData.val().length >= 4"}, "text": {".validate": "newData.isString() && newData.val().length <= 512 && newData.val().length >= 10"}, "time": {".validate": "newData.isNumber() && newData.val() <= now && newData.val() >= now - 15000"}, "$other": {".validate": false } } } } }
3. What have I learned/noticed
- Firebase realtime database doesn't seem to be very configurable regarding spam prevention, ddos, etc, at first sight at least.
- It has authentication built in, which I didn't want to use.
- So… there is this weird bug if you leave the browser's developers tools open the firebase client wont authenticate. Is that a bug or some sort very bad "anti hacking" thing they try :P
- Firestore has more methods while the realtime database is simpler and more suitable for small projects. For example I had to give up on having a proof of work ddos prevention because I can't compute a hash on the realtime database rules. There is a very limited scripting you can do there.
You can check bellow my first tests of the this feature.
Comments
Be the first to comment!