- 0. Files Tree
- 1. Setting thins up!
- 2. WebSockets protocols: Full Duplex Communication
- 3. Socket.io Events
- 4. Broadcasting Events
- 5. Sharing Location: MDN Geolocation API
- 6. Socket Acknowledgements!
- 7. Forms and Button (with Acknowledgements)
- 8. Rendering Messages with
Mustache - 9. Rendering Location
- 10. timestamp: Messing with Time and
MomentJS - 11. Styling the Chat App (
css) - 12. Adding a login page (
chatbecomesindex!) - 13. Sockets.io Rooms
- 14. Storing Users: Part I (
JavaScriptfunctions) - 15. Storing Users: Part II (more
JavaScriptfunctions) - 16. Tracking Users joining and leaving
- 17. Sending messages to rooms
- 18. Rendering User List
- 19. Autoscrolling
- 20. Deployment
chatapp
|-src
| |-index.js
| |-utils
| | |-messages.js
| | |-users.js
|-public
| |-css
| | |-styles.css
| | |-styles.min.css
| |-img
| | |-favicon.png
| |-js
| | |-chat.js
| |-index.html
| |-chat.html
|-package-lock.json
|-package.json
|-README.json
- initialize the NodeJS project:
- create chatapp folder and run
npm init, all the values can be default
- create chatapp folder and run
- Build a Node server:
// chatapp/src/index.js:
// load express
const express = require('express') // npm i express@14.6.4
const path = require('path') // node module
const app = express() // initialize the express app
// server call from environment variable or port 3000
const port = process.env.PORT || 3000
// main folder/source to render
const publicDirectoryPath = path.join(__dirname, '../public')
// use express static middleware to setup the server
app.use(express.static(publicDirectoryPath))
// listening to the port
app.listen(port, () => {
console.log(`Server is up on port ${port}!`)
})- Setup
publicdirectory: createchatapp/public/index.htmlfile
// chatapp/public/index.html
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Chat App</title>
</head>
<body>
<h1>
Chat App!
</h1>
</body>
</html>- Setup Script
In
chatapp/package.json:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node src/index.js",
"dev": "nodemon src/index.js"
}
- install
nodemonas development dependency:npm i nodemon@1.18.7 --save-dev
Commands:
to run the project: npm run start
to run in development mode: npm run dev
-
bidirectional communication between the client and the server
-
webSocket protocol is different from http
-
persistent connection between the client and the server
-
install
socket.io:npm i socket.io@2.2.0 -
In
src/index.js
const http = require('http') // enables http protocol
const socketio = require('socket.io') // enable webSocket protocol
...
const app = express()
const server = http.createServer(app) // create server using http
const io = socketio(server) // use http server with socket.io
...
// print sth with socket
// io.on(e, f) : function f runs when event e occurs
io.on('connection', () => {
console.log(`message from webSocket connection!`)
}) // this requires the connection of the server to the client side
// connect socket.io in public/index.html
server.listen(port, () => {
console.log(`Server is up on port ${port}!`)
})- Server-Client connection for webSocket:
- in
public/index.html
- in
...
Chat App!
// client side version of the library for socket connection
// make a client side JS file and use the libraries provided by
// the following script
</body>
<script src="/socket.io/socket.io.js"></script>
<script src="/js/chat.js"></script>
...- Create
public/js/chat.jsand load the script
// public/js/chat.js
io() // provided by the socket.io script in the index.html
??? need to study more on this
- when some events occurs (for eg. 'connection') either on client side or on server side, we can ask socket to run certain task (for eg. sending a message)
- emit a message from a server side
on'connection' (when new client connects), - have that message displayed on the client side (have client listen the event)
- emit a message from a server side
// src/index.js
...
io.on('connection', (socket) => {
console.log('Message from webSocket connection!')
// when the event 'message' occurs 'Welcome!' message is sent
socket.emit('message', 'Welcome!')
})
... // public/js/chat.js
const socket = io()
// when the 'message' event occurs the function is executed that takes the message param
socket.on('message', (message) => {
console.log(message)
})- Now, create a form to get the input message and have that rendered on console
<!--public/index.html-->
...
Chat App!
<form id="message-form">
<input placeholder="message"></input>
<button>Send<button>
</form>
...- once the form is submitted, 'submit' event occurs (note, not explicitly mentioned in index.html); we need to grab that event from the client side
- Next, emit the event called 'sendMessage' with the input string
// public/chat.js
const socket = io()
socket.on('message', (message) => {
console.log(message)
})
// access the form using id and add the submit event
// e is a default event argument
document.querySelector('#message-form').addEventListener('submit', (e) ={
e.preventDefault() // prevent full page refresh
// grab the input string
// const message = document.querySelector('input').value
// this (grabbed by name tag) is less likely to break than querySelector
const message = e.target.elements.msg.value
// emit the event with the input message
socket.emit('sendMessage', message)
// make sure that the event 'sendMessage' is received in the server side
})- Receive the event 'sendMessage' in the server side
...
io.on('connection', (socket) => {
console.log(`Message from Socket connection!`)
socket.emit('message', 'Welcome! Client CONNECTED!')
// msg here is the message from the client-side event sendMessage
io.on('sendMessage', (msg) => {
socket.emit('message', msg)
})
})
...broadcasthelps integrate the status of the users- send a message
A new user has joined.to everyone except the one that just joined
...
socket.emit('message', 'Welcome! Client CONNECTED!')
socket.broadcast.emit('message', 'A new user has joined.')
...- So far, we have three ways a server can emit an event:
socket.emit: to emit the event to the particular connection,
socket.broadcast.emit: to emit to everyone except itself
io.emit: to emit to everyone
- Send message 'A user has left.' to everyone once the user leaves chat
// src/index.js
io.on('connection', (socket) => {
console.log(`Message from Socket connection!`)
socket.emit('message', 'Welcome! Client CONNECTED!')
socket.broadcast.emit('message', 'A new user has joined.')
socket.on('sendMessage', (msg) => {
io.emit('message', msg)
})
// use the built-in event 'disconnect', note it uses `socket` to emit this event
socket.on('disconnect', () => {
// since we want to let everyone know, we use emit
io.emit('message', 'A user has left.')
})
})- First, create a button
// public/index.html
...
<h1>
Chat App!
</h1>
<form id="message-form">
<input name="msg" placeholder="message" />
<button>Send</button>
</form>
<button id="send-location">Send Location</button>
...- Next, add the event 'click' in client-side for this button
// public/chat.js
...
document.querySelector("#send-location").addEventListener('click', () => {
// make sure that browser supports Geolocation
if ( !navigator.geolocation ) {
return alert('Your browser does not support Geolocation service.')
}
// get the user's Location
navigator.geolocation.getCurrentPosition( (position) => {
console.log(position)
})
})- Now, create an event 'sendLocation' and render the location in the browser, note that the event here is emitted inside the
navigator.geolocation.getCurrentPositionfunction. This function is provided by MDN Geolocation API.
// public/js/chat.js
...
navigator.geolocation.getCurrentPosition( (pos) => {
// Use this to fetch the current position, shown in JSON format
// console.log(position)
socket.emit('sendLocation', {
latitude: pos.coords.latitude,
longitude: pos.coords.longitude
})
})- Make sure that the event is received by the server
// index.js
...
socket.on('sendLocation', (position) => {
io.emit('message', `Location: ${position.latitude}, ${position.longitude}`)
})- The query format for Google map's location is
https://www.google.com/maps?q=latitude,longitude, wherelatitudeandlongitudeare actual coordinates. We now instead want to render this link.
// index.js
...
socket.on('sendLocation', (position) => {
io.emit('message', `https://www.google.com/maps?q=${position.latitude},${position.longitude}`)
})
...- Well acknowledgements are acknowledgements. Basically callback function provided to the event that acknowledge that event was occurred.
// public/js/chat.js
...
document.querySelector('#message-form').addEventListener('submit', (e) => {
e.preventDefault()
const message = e.target.elements.msg.value
// add the callback acknowledgement
socket.emit('sendMessage', message, () => {
console.log('The message was delivered.')
})
})
...- Now, with that client expects server to acknowledge this acknowledgement.
- note that callback can take as many parameter as we want,
- provide a message 'Acknowledged.' to callback as parameter
// index.js
...
// set up param callback that acknowledges
socket.on('sendMessage', (msg, callback) => {
io.emit('message', msg)
callback('Acknowledged.')
})
...- The parameter sent to
callback()in server can be fetched in the client sidechat.jsas
// public/js/chat.js
...
document.querySelector('#message-form').addEventListener('submit', (e) => {
e.preventDefault()
const message = e.target.elements.msg.value
// add the callback acknowledgement
socket.emit('sendMessage', message, (serverMessage) => {
console.log('The message was delivered.', `${serverMessage}`)
})
})
...- Acknowledgement allows to use npm module
bad-wordsto filter the message for profanity - To install:
npm i bad-words@3.0.0 - Check for the profanity in the message received from client
// index.js
...
const Filter = require('bad-words')
...
socket.on('sendMessage', (msg, callback) => {
const filter = new Filter()
if (!filter.isProfane(msg)) {
return callback('Profanity is not allowed here.')
}
io.emit('message', msg)
callback('Acknowledged.')
})
...- Change the callback function to incorporate Error
// public/js/chat.js
...
document.querySelector('#message-form').addEventListener('submit', (e) => {
e.preventDefault()
const message = e.target.elements.msg.value
socket.emit('sendMessage', message, (error) => {
if (error) {
console.log(error)
}
console.log('Message Delivered.')
})
})
...-
Acknowledgement for Sharing Location
-
set up a client acknowledgement function
-
set up the server to send back the acknowledgement
-
have the client print "Location shared!" when acknowledged
- add a function parameter in 'sendLocation'
- add callback function in server
// public/js/chat.js
...
navigator.geolocation.getCurrentPosition( (pos) => {
socket.emit('sendLocation', {
latitude: pos.coords.latitude,
longitude: pos.coords.longitude
}, () => {
console.log('Location shared.')
})
})// index.js
...
socket.on('sendLocation', (position, callback) => {
io.emit('message', `https://www.google.com/maps?q=${position.latitude},${position.longitude}`)
callback()
})
...- Enabling and disabling button between when the message is sent and when the message s received
- create elements for code efficiency
- once the message is sent, clear the input field, and add focus cursor
disable:
document.querySelector('#message-form').querySelector('button').setAttribute('disabled', 'disabled')
enable:
document.querySelector('#message-form').querySelector('button').removeAttribute('disabled')
clear input field:
document.querySelector('#message-form').querySelector('input').value = ''
set focus:
document.querySelector('#message-form').querySelector('input').focus()
// public/js/chat.js
...
// elements
const $messageForm = document.querySelector('#message-form')
const $messageFormInput = $messageForm.querySelector('input')
const $messageFormButton = $messageForm.querySelector('button')
...
$messageForm.addEventListener('submit', (e) => {
e.preventDefault
//disable the form
$messageFormButton.setAttribute('disabled', 'disabled')
const message = e.target.elements.msg.value
socket.emit('sendMessage', message, (error) => {
// enable the form
$messageFormButton.removeAttribute('disabled')
// clear input field
$messageFormInput.value = ''
// set focus
$messageFormInput.focus()
if (error) {
return console.log(error)
}
console.log('Message delivered.')
})
})- Now, do the same thing for the event 'sendLocation'. Note that there is only one element for '#send-location (why?)'.
disable:
document.querySelector('#send-location').setAttribute('disabled', 'disabled')
enable:
document.querySelector('#send-location').removeAttribute('disabled')
// public/js/chat.js
...
const $sendLocationButton = document.querySelector('#send-location')
...
$sendLocationButton.addEventListener('click', () => {
...
$sendLocationButton.setAttribute('disabled', 'disabled')
navigator.geolocation.getCurrentPosition( (pos) => {
socket.emit('sendLocation', {
...
}, () => {
$sendLocationButton.removeAttribute('disabled')
console.log('Location shared.')
})
})
})/Link to Libraries./
- Libraries:
- Mustache
- Moment
- QS
- import the libraries on index.html
<!-- /public/index.html -->
...
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/3.0.1/mustache.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qs/6.6.0/qs.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="/js/chat.js"></script>
</body>
</html>- Define a message rendering template in
index.html
<!-- index.html -->
<div id="messages"></div>
...
<script id="message-template" type="text/html">
<p>{{message}}<p>
</script>- render the message: you need
- the template
- the place where you render the message (in our case
<div id="messages">) - render inside the
'message'event - note that
innerHTMLtag inmessageTemplateis important in order to get the html content of the template
// chat.js
...
const $messages = document.querySelector('#messages') // grab the div
// grab the template as HTML
const messageTemplate = document.querySelector('#message-template').innerHTML
socket.on('message', (msg) => {
...
const html = Mustache.render(messageTemplate, {message: msg})
$messages.insertAdjacentHTML('beforeend', html)
})- This is similar to the rendering message
<!-- index.html -->
...
<script id="location-message-template" type="text/html">
<div>
<p><a href="{{url}}" target="_blank">my current location</a><p>
</div>
</script>
... // chat.js
...
const locationMessageTemplate = document.querySelector('#location-message-template').innerHTML
// location message is emitted at the server index.js
socket.on('locationMessage', (locationLink) => {
const html = Mustache.render(locationMessageTemplate, {url:locationLink})
$messages.insertAdjacentHTML('beforeend', html)
})- javascript provides some built-in time functionality. This can be tested in the console provided with the web developer tools
const now = new Date()
now.toString() // "Tue Feb 25 2020 14:50:51 GMT-0500 (Eastern Standard Time)"
now.getDate() // 25
now.getTime() // 1582660251241 == 50.18 years
getTime()provides the number of milliseconds since UNIX epic (midnight Jan 1st, 1970). Positive number go into future and negative number go to past from the epic.- Display the time of the message: Emit two things-- message and the timestamp
- by providing two arguments (message and timestamp) to the event,
- OR by providing single object as argument (message and timestamp would be object properties).
// index.js
...
socket.emit('message', {
text: "Welcome! You're connected.",
createdAt: new Date().getTime()
})
...- Create the function that generates the above object (this is because we don't have to type all the code every time we need the object. efficiency!).
- create
chatapp/src/utils/messages.js
- create
// src/utils/messages.js
// the function takes the message `text` as argument and returns an object with
// `text` and `timestamp`
const generateMessage = (text) => {
return {
text,
createdAt: new Date().getTime()
}
}
module.exports = {
generateMessage
}- Now, call the function in server:
// index.js
...
const { generateMessage } = require('./utils/messages')
socket.emit('message', generateMessage('Welcome! You\'re connected.'))
...- Now, we need to adjust the client side as well
// chat.js
...
// msg now is an object
// to render actual message we call msg.txt
socket.on('message', (msg) => {
console.log(msg)
// render msg inside the messageTemplate
const html = Mustache.render(messageTemplate, {
message: msg.txt
})
$messages.insertAdjacentHTML('beforeend', html)
})
...- We can use
generateMessage()in all the four message emit events in server.
// index.js
...
// to the users joining
socket.emit('message', generateMessage('Welcome!, You\'re connected.'))
// to all the other users
socket.broadcast.emit('message', generateMessage('A new user has joined.'))
...
// when the users leaves room
socket.on('disconnet', () => {
io.emit('message', generateMessage('A user has left.'))
})
...- Integrate timestamp on the message-template
<!-- index.html -->
...
<script id="message-template" type="text/html">
<div>
<p>{{createdAt}} - {{message}}</p>
</div>
</script>
...- Adjust the object (add timestamp) that is passed into the template.
- Right now timestamp is just a big number, we need to show this in an understandable manner. That's where
momentjs(imported earlier as one of the js dependency in index.html) comes to play.- go to MomentJS,
- go to docs/display,
- check the tokens used to format the dates and time by using
moment().format(), - the timestamp is passed into the
moment()
- go to MomentJS,
// chat.js
socket.on('message', (msg) => {
console.log(msg)
// render msg inside the messageTemplate
const html = Mustache.render(messageTemplate, {
message: msg.txt,
createdAt: moment(msg.createdAt).format('hh:mm a')
})
$messages.insertAdjacentHTML('beforeend', html)
})[Note: The timestamp can be added to location messages in similar way.]
- import the stylesheets to html
- Note that
styles.min.cssruns faster than thestyles.css- this will change the style of the chatapp in some way
<!-- index.html -->
<head>
...
<link rel="icon" href="/img/favicon.png">
<link rel="stylesheet" href="/css/styles.min.css">
</head>- Now to style the
<body >of the the Chat app, create a<div >, inside which there will be two more<div >s:chat__sidebar(for sidebar) andchat__main(for main messaging section).- move the messaging section into the
chat__maindiv
- move the messaging section into the
- More adjustments: make the other adjustments as shown below.
<!-- index.html -->
...
<body>
<div class="chat">
<div class="chat__sidebar">
</div>
<div class="chat__main">
<div id="messages" class="chat__messages">
</div>
<div class="compose">
<form id="message-form">
<input name="msg" placeholder="Message" /> <!--input-->
<button>Send</button> <!--button-->
</form>
<button id="send-location">Send Location</button>
</div>
</div>
</div>
<script id="message-template" type="text/html">
<div class="message">
<p>
<span class="message__name">User_name</span>
<span class="message__meta">{{createdAt}}</span>
</p>
<p>{{message}}</p>
</div>
</script>
<script id="location-message-template" type="text/html">
<div class="message">
<p>
<span class="message__name">User_name</span>
<span class="message__meta">{{createdAt}}</span>
</p>
<p><a href="{{url}}" target="_blank">my current location</a></p>
</div>
</script>- For this, we want our main index page to be a log in form where users can log in. To do this create a new
/public/chat.htmlfile and and move all the code fromindex.htmltochat.html.index.htmlis modified to display a user's log in form.
- when you log in as
bitmin roomVB2020you get the chat room link that is automatically generated ashttp://localhost:3000/chat.html?username=bitm&room=VB2020. In order to setup the chatroom that multiple users can access to we need to parse the string after the main url i.e.?username=bitm&room=VB2020. - To check this in console
location.origin: "http://localhost:3000"
location.search: "?username=bitm&room=VB2020"
- We'll be using
qs.js(query string) to parse the string. - To Parse:
{ ignoreQueryPrefix: true }removes the?sign in front of the string
- Once we have our
usernameandroomset, we can emit new event for joining a specific room.
// chat.js
...
// options
// returns strings username and room
const { username, room } = Qs.parse(location.search, { ignoreQueryPrefix: true})
...
// emit the event 'join'
socket.emit('join', {username, room})- We've just parsed the string and get username and room as object. We've emitted an event called 'join' with those arguments.
- Now, in the server side set up a listener for this event.
- To join the individual chat room, use Socket' join method that can only be used in server:
socket.join(). This gives access to emitting any events to just this specific room. - Here, with rooms, we introduce two more methods of emitting the message
socket.emit() - to emit the event to the particular connection, socket.broadcast.emit() - to emit to everyone except itself io.emit() - to emit to everyone io.to.emit() - to everyone in a specific chatroom socket.broadcast.to.emit() - to everyone except itself in a specific chatroom- Remember the two methods for 'message' event:
socket.emit()andsocket.broadcast.emit(); since we are incorporating room now we want to modify this methods with.to.emit()and put them inside thejoinevent.
- To join the individual chat room, use Socket' join method that can only be used in server:
// server.js
socket.on('join', ({username, room}) => {
socket.join(room)
// modified version of message events with .to.emit()
// the first one is fine because it's emitting the message to specific socket
socket.emit('message', generateMessage('Welcome!You\'re connected.'))
// For this, we need to add .to.emit(). Otherwise, it will send message to everyone
//regardless of what room they are in
socket.broadcast.to(room).emit('message', generateMessage(`${username} has joined.`))
})- So far
- you can join separate rooms,
- when a user logs in a room, everyone else except the user get the message saying
${username} has joined. - But,
- while sending messages message goes to all the active rooms, instead of a specific chatroom
- Let's fix this.
-
In order to send the message to specific room, we need to to keep track of which users are active in which rooms.
-
Note that the objects properties returned by the qs parser,
{ username, room }, as of now are only accessible in the event 'join.'- create 'src/utils/users.js'
- methods implemented:
users[] addUser() removeUser() getUser() getUsersInRoom()
// src/utils/users.js
const users = []
// takes a user object {id,username,room}, validates,
// stores in the array users[], and returns the object
const addUser = ( { id, username, room }) => {
// Clean the input
username = username.trim().toLowerCase()
room = room.trim().toLowerCase()
// validation
if ( !username || !room ) {
return {
error: 'Username and room are required!'
}
}
// check for existing user
// iterates over the users[] array and checks
// if username and room match
const existingUser = users.find( (user_in_users) => {
return user_in_users.username === username && user_in_users.room === room
})
if ( existingUser ) {
return {
error: 'Username already in use!'
}
}
// store user
const user = { id, username, room }
users.push(user)
return { user }
}
// removing the user with it's id
const removeUser = (id) => {
// iterate over the users[] array and
// get the index of the user with given id
// shorthand: const index = users.findIndex( (user) => user.id === id)
// returns -1 if not found
const index = users.findIndex( (user) => {
return user.id === id
})
if ( index != -1) {
// users.splice(index, 1) is an array of objects that are
// removed from the users[] array; however, since we are removing
// only one `(index, 1)` object [o] is provided at the end
return users.splice(index, 1)[0]
}
return {
error: 'User doesn\'t exist!'
}
}- Here we implement
getUser() getUsersInRoom()
// src/utils/users.js
...
// accepts the id and returns
// the user object with that id
const getUser = (id) => {
// check if the user exist or not
return users.find((user) => user.id === id
)
}
// returns all the users object in the given room
const getUsersInRoom = (room) => {
return users.filter((user) => user.room === room)
}
...- Don't forget to export the functions
- Now we have
addUserwe use this first.- we can get either
errororuserfrom theaddUser() - if
error, we send the acknowledgement to the client through callback
- we can get either
// src/index.js
...
io.on('connection', (socket) => {
console.log('Message from Socket connection!')
// add callback
socket.on('join', ( { username, room}, callback ) => {
// add the user
const {user, error} = addUser({id: socket.id, username, room})
if (error) {
return callback(error)
}
socket.join(user.room)
socket.emit('message', generateMessage('Welcome! You\'re connected.'))
socket.broadcast.to(user.room)
.emit('message',generateMessage(`${user.username} has joined.`))
// add callback
callback()
})
}) // Note: {username, room} can be destructured as:
socket.on('join', (options, callback ) => {
// add the user
const {user, error} = addUser({id: socket.id, ...options})
if (error) {
return callback(error)
}
...
}) // public/chat.js
...
// add the callback in case of error
socket.emit('join', { username, room }, (error) => {
})- Now we want to remove the user from the room once the user disconnects, but note we want to emit the message only when an actual user disconnects
// index.js
...
socket.on('disconnect', () => {
const user = removeUser(socket.id)
if ( user ) {
io.to(user.room).emit('message', `${user.username} has left!`)
}
})
...- Now we can also handle errors when there is error such as two same username joining the room. This is done with the callback function at the client side.
- If there is error we show what went wrong and we redirect the user to the home page using
location.href
- If there is error we show what went wrong and we redirect the user to the home page using
// public/chat.js
...
socket.emit('join', {username, room}, ( error ) => {
if ( error ) {
alert(error)
location.href = '/'
}
})- use
getUser(socket.id)to get the user and useto(user.room)in eventssendMessageandsendLocation. - Display the name of the users in the chatroom:
- Note in
generateMessage()andgenerateLocationMessage()function inmessages.jsthe only one argument passed, - add another argument
usernameon both, - access
usernamein index and chat, - modify both
generateMessage()andgenerateLocationMessage(), - in case of system send message passed in
admininstead ofusername
- Note in
- left
// Public/js/chat.js
...
const autoScroll = () => {
// new message element
const $newMessage = $messages.lastElementChild
// Height of the new Message
const newMessageStyles = getComputedStyle($newMessage)
// console.log(newMessageStyles)
const newMessageMargin = parseInt(newMessageStyles.marginBottom)
const newMessageHeight = $newMessage.offsetHeight + newMessageMargin
// visible height
const visibleHeight = $messages.offsetHeight
// Height of message container
const containerHeight = $messages.scrollHeight
// How far have I scrolled?
const scrollOffset = $messages.scrollTop + visibleHeight
if (containerHeight - newMessageHeight <= scrollOffset) {
$messages.scrollTop = $messages.scrollHeight
}
}
...- Create Heroku Account
- Install the Heroku CLI
- run
heroku login - setup ssh to securely integrate the codes:
heroku keys:add
git remotechecks the remotegit push heroku master