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
Select "Node.JS", and then enter your desired title
You should see something like this:
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:
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.
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 moduleconst app = express();
- Initializes the ExpressJS moduleconst fs = require('fs');
- Module for editing and making changes to filesconst http = require('http').Server(app);
- Imports the HTTP moduleconst io = require('socket.io')(http);
- Imports the socket.io moduleconst port = process.env.PORT || 3000;
- Sets the default port to listenvar 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.
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 rannpm install socket.io
var socket = io();
- Initializes socket.io on client sidevar 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 JSif($_GET['darkMode']) {document.documentElement.classList.add("darkMode")}
- Add the "darkMode" parameter to the url to render the chat in dark modewindow.onerror=function(e){socket.emit('err',e)}
- Useful for debuggingvar 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 defaultsocket.on('msg', function(msg,room) { ... })
- Function for when a user sends a messagevar 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 connectedsocket.on('msg', (msg, room) => { ... })
- Callback for when a user sends a messageio.emit('msg', filter.clean(msg), room);
- Returns the message back to the uservar db = JSON.parse(fs.readFileSync(
./public/db.json));
- Imports the databaseif(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 databasefilter.clean(msg)
- Removes profanity from the message.fs.writeFileSync("./public/db.json", JSON.stringify(db, null, null))
- Writes to the databasesocket.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!