ManuTheCoder
ManuTheCoder's Blog

ManuTheCoder's Blog

Creating a simple chat with Node.JS with repl.it!

Creating a simple chat with Node.JS with repl.it!

ManuTheCoder's photo
ManuTheCoder
·Oct 31, 2021·

7 min read

Subscribe to my newsletter and never miss my upcoming articles

Listen to this article

Have you ever wanted to build a chat app for your website, but never understood any of the YouTube tutorials, or articles on the internet? This article is for you!

Friendly assumptions

In this article, I'll assume you should be knowing these simple things:

  • You know how to use a computer, and navigate IDE's and browsers
  • You know JavaScript
  • You have a basic understanding of HTML and CSS

Step 1

For this app, let's use repl.it. It's a great in-browser IDE. Sign in/Sign up with your GitHub account.

  • Once you're done, click on the "Create Repl" button in the side navigation menu image.png

  • Select "Node.JS", and then enter your desired title image.png

  • You should see something like this: image.png

If you see this screen, great job! You've set up repl.it. It's now time to code!

Step 2

For this app, we'll use socket.io, express.js, and bad-words.

  • Click on the "Shell" tab, and enter the following commands. Hit enter after each one.
    npm install socket.io
    
    npm install express
    
    npm install bad-words
    

Once done, it should look like this: image.png

Great job! You've installed the packages. Onward!

Step 3

Let's set up our files. Create a file/folder by clicking the buttons next to the "Files" section. Don't worry about "Packager Files". You can safely ignore them. image.png

Step 4

Let's set up a web server using ExpressJS!

In your index.js file, insert the following code:

const express = require('express');
const app = express();
const fs = require('fs');
const http = require('http').Server(app);
const io = require('socket.io')(http);
const port = process.env.PORT || 3000;
var Filter = require('bad-words'),
    filter = new Filter();

Explained

  • const express = require('express'); - This imports the expressjs module
  • const app = express(); - Initializes the ExpressJS module
  • const fs = require('fs'); - Module for editing and making changes to files
  • const http = require('http').Server(app); - Imports the HTTP module
  • const io = require('socket.io')(http); - Imports the socket.io module
  • const port = process.env.PORT || 3000; - Sets the default port to listen
  • var Filter = require('bad-words'), filter = new Filter(); - Imports the profanity filter module

Now, let's create a static server:

app.use(express.static('public'));
http.listen(port, () => {
  console.log(`Socket.IO server running at http://localhost:${port}/`);
});

As the code says above, the default directory is public.

Now, click the Run button at the top. image.png

Great! Now, you should see a blank web page!

Step 5

Let's create the user interface

  • We'll be using MaterializeCSS for this app. It's a great material design JS/CSS framework.

Insert this code into /public/index.html.

<!DOCTYPE html>
<html>
  <head>
    <title>Socket.IO chat</title>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@materializecss/materialize@1.1.0-alpha/dist/css/materialize.min.css">
        <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons|Material+Icons+Outlined|Material+Icons+Two+Tone|Material+Icons+Round|Material+Icons+Sharp">
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
        <style>
            textarea {resize: vertical;transition: all .2s;scroll-behavior:smooth;max-height: 30vh;margin-right: 10px!important;}
            .message {padding: 10px;transform-origin: center;transition: all .2s}
            * {box-sizing: border-box}
            #msgBar {display: block;position: fixed;bottom: 0;left:0;width:100%;background:transparent;padding:0;align-items: center;padding:10px;}
            #msgBar textarea {background: #fff}
            #msgBar textarea {box-shadow: 0 0 10px #ccc !important;border: 0 !important;border-radius: 25px;padding-left: 20px;width: calc(100% - 65px)}
            .btn-floating[disabled] {color: #000 !important;background: #eee !important;box-shadow: none !important}
            body {
                padding-bottom: 65px;
            }
            .waves-effect:not(.waves-light, ._darkTheme .waves-effect) .waves-ripple {
                background: rgba(0, 0, 0, .2) !important
            }
            ._darkTheme .waves-ripple {background: rgba(255, 255, 255, .2) !important}
            .waves-light .waves-ripple {background: rgba(255, 255, 255, .2) !important}
            nav a,nav a i {
                background: transparent !important;
                line-height: 65px !important
            }
            .waves-center .waves-ripple {
                top: 50% !important;
                left: 50% !important
            }
            nav .waves-ripple {
                transition: all .5s !important
            }
            .waves-ripple {
                transition: transform .8s cubic-bezier(0.4, 0, 0.2, 1), opacity .4s !important
            }
            .message {
                width: 100%;
                animation: msg .2s forwards;
            }
            @keyframes msg {
                0% {transform: translateY(10px);opacity:0}
            }
            .copied::after {
                content: "Copied!";
                float: right;
                background: #37474f;
                color: white;
                padding: 4px 10px;
                animation: opacity 1s fowards;
                border-radius: 999px;
            }
            @keyframes opacity {
                0% {transform: scale(0)}
                50% {transform: scale(1)}
                100% {transform: scale(0)}
            }
            .loader #spinner { box-sizing: border-box; stroke: #000; stroke-width: 3px; -webkit-transform-origin: 50%; transform-origin: 50%; -webkit-animation: line 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite, rotate 1.6s linear infinite; animation: line 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite, rotate 1.6s linear infinite; } @-webkit-keyframes rotate { from { -webkit-transform: rotate(0); transform: rotate(0); } to { -webkit-transform: rotate(450deg); transform: rotate(450deg); } } @keyframes rotate { from { -webkit-transform: rotate(0); transform: rotate(0); } to { -webkit-transform: rotate(450deg); transform: rotate(450deg); } } @-webkit-keyframes line { 0% { stroke-dasharray: 2, 85.964; -webkit-transform: rotate(0); transform: rotate(0); } 50% { stroke-dasharray: 65.973, 21.9911; stroke-dashoffset: 0; } 100% { stroke-dasharray: 2, 85.964; stroke-dashoffset: -65.973; -webkit-transform: rotate(90deg); transform: rotate(90deg); } } @keyframes line { 0% { stroke-dasharray: 2, 85.964; -webkit-transform: rotate(0); transform: rotate    (0); } 50% { stroke-dasharray: 65.973, 21.9911; stroke-dashoffset: 0; } 100% { stroke-dasharray: 2, 85.964; stroke-dashoffset: -65.973; -webkit-transform: rotate(90deg); transform: rotate(90deg); } }
            .darkMode .loader #spinner {stroke: #fff !important}
            .darkMode body {background: #212121 !important;color: white}
            .darkMode #input {box-shadow: 0 0 10px rgba(0,0,0,0.2) !important;color: white;background: #303030}
            .darkMode [disabled] {background: #404040 !important}
        </style>
  </head>
  <body>
    <div class="container">
            <div id="messages">
                <center>
                    <br><br><br>
                    <br><br><br>
                    <div class="loader">
                        <svg viewBox="0 0 32 32" width="42" height="42">
                                <circle id="spinner" cx="16" cy="16" r="14" fill="none"></circle>
                            </svg>
                    </div>
                    <br><br><br>
                    <br><br><br>
                </center>
            </div>
    </div>
    <form id="msgBar" action="">
            <button class="btn-floating waves-effect waves-light blue-grey darken-3 right waves-center" id="send">
                <i class="material-icons">send</i>
            </button>
            <textarea placeholder="Type..." autofocus class="materialize-textarea" id="input" autocomplete="off" onkeyup="if(event.keyCode==13&&!event.shiftKey){this.value=this.value.trim();$('#send').click()}"></textarea>
    </form>
        <script src="https://cdn.jsdelivr.net/npm/@materializecss/materialize@1.1.0-alpha/dist/js/materialize.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/emoji-button@latest/dist/index.min.js"></script>
        <script src="https://cdn.jsdelivr.net/gh/ManuTheCoder/JS-Essentials/essentials.min.js"></script>
        <audio src="https://padlet-uploads.storage.googleapis.com/446844750/2d5accc0b66f1e5951cf186f1b981701/notification_simple_01__AudioTrimmer_com_.wav" id="chatSound"></audio>
    <script src="/socket.io/socket.io.js"></script>

    <script>
            var parts = window.location.search.substr(1).split("&");
            var $_GET = {};
            for (var i = 0; i < parts.length; i++) {
                    var temp = parts[i].split("=");
                    $_GET[decodeURIComponent(temp[0])] = decodeURIComponent(temp[1]);
            }
            if($_GET['darkMode']) {document.documentElement.classList.add("darkMode")}            
            window.onerror=function(e){socket.emit('err',e)}
      var socket = io();
if(! $_GET['id']) { $_GET['id']="undefined"}            
      var messages = document.getElementById('messages');
      var form = document.getElementById('msgBar');
      var input = document.getElementById('input');

            form.addEventListener('submit', function(e) {
        e.preventDefault();
        if (input.value) {
                    document.getElementById("send").disabled = true;
                    setTimeout(() => {
                        document.getElementById("send").disabled = false
                    }, 500)
          socket.emit('msg', input.value, $_GET['id']);
          input.value = '';
        }
      });

      socket.on('msg', function(msg,room) {
                if(room=$_GET['id']) {
                document.getElementById('chatSound').play()
        messages.insertAdjacentHTML("beforeend", `
                <div class="message waves-effect" onclick="navigator.clipboard.writeText(this.innerText);this.classList.add('copied');">${msg}</div>
                `);
                window.scrollTo(0,document.body.scrollHeight);
                }
      });

            var xhttp = new XMLHttpRequest();
            xhttp.onreadystatechange = function() {
                if (this.readyState == 4 && this.status == 200) {
                    var db = JSON.parse(this.responseText);
                    if(db[$_GET['id']]) {
                        messages.innerHTML = `<br><br>`
                        db[$_GET['id']].messages.forEach(e => {
                            messages.insertAdjacentHTML("beforeend", `
                            <div class="message waves-effect" onclick="navigator.clipboard.writeText(this.innerText);this.classList.add('copied');">${e}</div>
                            `);
                            window.scrollTo(0,document.body.scrollHeight);
                        })
                    }
                    else {
                        messages.innerHTML = `<br><br>`
                    }
                }
            };
            xhttp.open("GET", "db.json", true);
            xhttp.send();

    </script>
  </body>
</html>

Explained

  • <div id="messages">...</div> - This is where the messages load
  • <audio src="https://padlet-uploads.storage.googleapis.com/446844750/2d5accc0b66f1e5951cf186f1b981701/notification_simple_01__AudioTrimmer_com_.wav" id="chatSound"></audio> - Just an audio file for a chat sound
  • <script src="/socket.io/socket.io.js"></script> - Imports the socket.io file automatically created when you ran npm install socket.io
  • var socket = io(); - Initializes socket.io on client side
  • var parts = window.location.search.substr(1).split("&"); var $_GET = {}; for (var i = 0; i < parts.length; i++) { var temp = parts[i].split("="); $_GET[decodeURIComponent(temp[0])] = decodeURIComponent(temp[1]); } - Allows you to access url parameters directly from JS
  • if($_GET['darkMode']) {document.documentElement.classList.add("darkMode")} - Add the "darkMode" parameter to the url to render the chat in dark mode
  • window.onerror=function(e){socket.emit('err',e)} - Useful for debugging
  • var messages = document.getElementById('messages'); - Selects the element with an id of "messages", and then stores it into the variable "messages"
  • form.addEventListener('submit', function(e) { ... }) - Prevents the form from submitting by default
  • socket.on('msg', function(msg,room) { ... }) - Function for when a user sends a message
  • var xhttp = new XMLHttpRequest(); - Fetches chat history from database file, /public/db.json

Step 6

Let's implement it on the server side now!

Add this code to your index.js file.

io.on('connection', (socket) => {
  socket.on('msg', (msg, room) => {
    io.emit('msg', filter.clean(msg), room);
        var db = JSON.parse(fs.readFileSync(`./public/db.json`));
        if(db[room]) {
            db[room].messages.push(filter.clean(msg))
        }
        else {
            db[room] = {};
            db[room].messages = JSON.parse(`[${JSON.stringify(filter.clean(msg))}]`)
        }
        // Replace the last "null" parameter with "\t" for pretty printing
        fs.writeFileSync("./public/db.json", JSON.stringify(db, null, null))
  });
    socket.on('err', msg => {
    console.log(msg)
  });
});

Explained

  • io.on('connection', (socket) => { ... }) - Callback for when the socket is connected
  • socket.on('msg', (msg, room) => { ... }) - Callback for when a user sends a message
  • io.emit('msg', filter.clean(msg), room); - Returns the message back to the user
  • var db = JSON.parse(fs.readFileSync(./public/db.json)); - Imports the database
  • if(db[room]) { db[room].messages.push(filter.clean(msg)) } else { db[room] = {}; db[room].messages = JSON.parse([${JSON.stringify(filter.clean(msg))}]) } - Creates an object in the database
  • filter.clean(msg) - Removes profanity from the message.
  • fs.writeFileSync("./public/db.json", JSON.stringify(db, null, null)) - Writes to the database
  • socket.on('err', msg => { console.log(msg) }); - Logs errors in the console

Yayyy!!!! You did it!

Hit CTRL + Enter, or just restart the app!

Here are a few features:

  • Add a "room" parameter to create a different chat room. Example: https://youareawesome.net?id=1
  • Add the "darkMode" parameter to render the chat in a dark theme. Example: https://youaareawesome.net?id=1&darkMode=true

Is the code not working? Have any compliments? Let me know in the comments below!

See a live demo here: replit.com/@ManuTheCoder/Chat-App#index.js

 
Share this