├── _html_css ├── js │ └── main.js ├── index.html ├── chat.html └── css │ └── style.css ├── .gitignore ├── .replit ├── utils ├── messages.js └── users.js ├── README.md ├── package.json ├── public ├── index.html ├── chat.html ├── js │ └── main.js └── css │ └── style.css └── server.js /_html_css/js/main.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | .env -------------------------------------------------------------------------------- /.replit: -------------------------------------------------------------------------------- 1 | language = "nodejs" 2 | run = "npm run dev" -------------------------------------------------------------------------------- /utils/messages.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | 3 | function formatMessage(username, text) { 4 | return { 5 | username, 6 | text, 7 | time: moment().format('h:mm a') 8 | }; 9 | } 10 | 11 | module.exports = formatMessage; 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatCord App 2 | Realtime chat app with websockets using Node.js, Express and Socket.io with Vanilla JS on the frontend with a custom UI 3 | [![Run on Repl.it](https://repl.it/badge/github/bradtraversy/chatcord)](https://repl.it/github/bradtraversy/chatcord) 4 | ## Usage 5 | ``` 6 | npm install 7 | npm run dev 8 | 9 | Go to localhost:3000 10 | ``` 11 | 12 | ## Notes 13 | The *_html_css* folder is just a starter template to follow along with the tutorial at https://www.youtube.com/watch?v=jD7FnbI76Hg&t=1339s. It is not part of the app 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatcord", 3 | "version": "1.0.0", 4 | "description": "Realtime chat app with rooms", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server", 8 | "dev": "nodemon server" 9 | }, 10 | "author": "Brad Traversy", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@socket.io/redis-adapter": "^7.1.0", 14 | "dotenv": "^14.3.2", 15 | "express": "^4.17.1", 16 | "moment": "^2.24.0", 17 | "redis": "^4.0.2", 18 | "socket.io": "^4.4.1" 19 | }, 20 | "devDependencies": { 21 | "nodemon": "^2.0.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /utils/users.js: -------------------------------------------------------------------------------- 1 | const users = []; 2 | 3 | // Join user to chat 4 | function userJoin(id, username, room) { 5 | const user = { id, username, room }; 6 | 7 | users.push(user); 8 | 9 | return user; 10 | } 11 | 12 | // Get current user 13 | function getCurrentUser(id) { 14 | return users.find(user => user.id === id); 15 | } 16 | 17 | // User leaves chat 18 | function userLeave(id) { 19 | const index = users.findIndex(user => user.id === id); 20 | 21 | if (index !== -1) { 22 | return users.splice(index, 1)[0]; 23 | } 24 | } 25 | 26 | // Get room users 27 | function getRoomUsers(room) { 28 | return users.filter(user => user.room === room); 29 | } 30 | 31 | module.exports = { 32 | userJoin, 33 | getCurrentUser, 34 | userLeave, 35 | getRoomUsers 36 | }; 37 | -------------------------------------------------------------------------------- /_html_css/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | ChatCord App 15 | 16 | 17 |
18 |
19 |

ChatCord

20 |
21 |
22 |
23 |
24 | 25 | 32 |
33 |
34 | 35 | 43 |
44 | 45 |
46 |
47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | ChatCord App 15 | 16 | 17 |
18 |
19 |

ChatCord

20 |
21 |
22 |
23 |
24 | 25 | 32 |
33 |
34 | 35 | 43 |
44 | 45 |
46 |
47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /public/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | ChatCord App 14 | 15 | 16 |
17 |
18 |

ChatCord

19 | Leave Room 20 |
21 |
22 |
23 |

Room Name:

24 |

25 |

Users

26 |
    27 |
    28 |
    29 |
    30 |
    31 |
    32 | 39 | 40 |
    41 |
    42 |
    43 | 44 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /_html_css/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ChatCord App 9 | 10 | 11 |
    12 |
    13 |

    ChatCord

    14 | Leave Room 15 |
    16 |
    17 |
    18 |

    Room Name:

    19 |

    JavaScript

    20 |

    Users

    21 |
      22 |
    • Brad
    • 23 |
    • John
    • 24 |
    • Mary
    • 25 |
    • Paul
    • 26 |
    • Mike
    • 27 |
    28 |
    29 |
    30 |
    31 |

    Brad 9:12pm

    32 |

    33 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Eligendi, 34 | repudiandae. 35 |

    36 |
    37 |
    38 |

    Mary 9:15pm

    39 |

    40 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Eligendi, 41 | repudiandae. 42 |

    43 |
    44 |
    45 |
    46 |
    47 |
    48 | 55 | 56 |
    57 |
    58 |
    59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /public/js/main.js: -------------------------------------------------------------------------------- 1 | const chatForm = document.getElementById('chat-form'); 2 | const chatMessages = document.querySelector('.chat-messages'); 3 | const roomName = document.getElementById('room-name'); 4 | const userList = document.getElementById('users'); 5 | 6 | // Get username and room from URL 7 | const { username, room } = Qs.parse(location.search, { 8 | ignoreQueryPrefix: true, 9 | }); 10 | 11 | const socket = io(); 12 | 13 | // Join chatroom 14 | socket.emit('joinRoom', { username, room }); 15 | 16 | // Get room and users 17 | socket.on('roomUsers', ({ room, users }) => { 18 | outputRoomName(room); 19 | outputUsers(users); 20 | }); 21 | 22 | // Message from server 23 | socket.on('message', (message) => { 24 | console.log(message); 25 | outputMessage(message); 26 | 27 | // Scroll down 28 | chatMessages.scrollTop = chatMessages.scrollHeight; 29 | }); 30 | 31 | // Message submit 32 | chatForm.addEventListener('submit', (e) => { 33 | e.preventDefault(); 34 | 35 | // Get message text 36 | let msg = e.target.elements.msg.value; 37 | 38 | msg = msg.trim(); 39 | 40 | if (!msg) { 41 | return false; 42 | } 43 | 44 | // Emit message to server 45 | socket.emit('chatMessage', msg); 46 | 47 | // Clear input 48 | e.target.elements.msg.value = ''; 49 | e.target.elements.msg.focus(); 50 | }); 51 | 52 | // Output message to DOM 53 | function outputMessage(message) { 54 | const div = document.createElement('div'); 55 | div.classList.add('message'); 56 | const p = document.createElement('p'); 57 | p.classList.add('meta'); 58 | p.innerText = message.username; 59 | p.innerHTML += `${message.time}`; 60 | div.appendChild(p); 61 | const para = document.createElement('p'); 62 | para.classList.add('text'); 63 | para.innerText = message.text; 64 | div.appendChild(para); 65 | document.querySelector('.chat-messages').appendChild(div); 66 | } 67 | 68 | // Add room name to DOM 69 | function outputRoomName(room) { 70 | roomName.innerText = room; 71 | } 72 | 73 | // Add users to DOM 74 | function outputUsers(users) { 75 | userList.innerHTML = ''; 76 | users.forEach((user) => { 77 | const li = document.createElement('li'); 78 | li.innerText = user.username; 79 | userList.appendChild(li); 80 | }); 81 | } 82 | 83 | //Prompt the user before leave chat room 84 | document.getElementById('leave-btn').addEventListener('click', () => { 85 | const leaveRoom = confirm('Are you sure you want to leave the chatroom?'); 86 | if (leaveRoom) { 87 | window.location = '../index.html'; 88 | } else { 89 | } 90 | }); 91 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const http = require("http"); 3 | const express = require("express"); 4 | const socketio = require("socket.io"); 5 | const formatMessage = require("./utils/messages"); 6 | const createAdapter = require("@socket.io/redis-adapter").createAdapter; 7 | const redis = require("redis"); 8 | require("dotenv").config(); 9 | const { createClient } = redis; 10 | const { 11 | userJoin, 12 | getCurrentUser, 13 | userLeave, 14 | getRoomUsers, 15 | } = require("./utils/users"); 16 | 17 | const app = express(); 18 | const server = http.createServer(app); 19 | const io = socketio(server); 20 | 21 | // Set static folder 22 | app.use(express.static(path.join(__dirname, "public"))); 23 | 24 | const botName = "ChatCord Bot"; 25 | 26 | (async () => { 27 | pubClient = createClient({ url: "redis://127.0.0.1:6379" }); 28 | await pubClient.connect(); 29 | subClient = pubClient.duplicate(); 30 | io.adapter(createAdapter(pubClient, subClient)); 31 | })(); 32 | 33 | // Run when client connects 34 | io.on("connection", (socket) => { 35 | console.log(io.of("/").adapter); 36 | socket.on("joinRoom", ({ username, room }) => { 37 | const user = userJoin(socket.id, username, room); 38 | 39 | socket.join(user.room); 40 | 41 | // Welcome current user 42 | socket.emit("message", formatMessage(botName, "Welcome to ChatCord!")); 43 | 44 | // Broadcast when a user connects 45 | socket.broadcast 46 | .to(user.room) 47 | .emit( 48 | "message", 49 | formatMessage(botName, `${user.username} has joined the chat`) 50 | ); 51 | 52 | // Send users and room info 53 | io.to(user.room).emit("roomUsers", { 54 | room: user.room, 55 | users: getRoomUsers(user.room), 56 | }); 57 | }); 58 | 59 | // Listen for chatMessage 60 | socket.on("chatMessage", (msg) => { 61 | const user = getCurrentUser(socket.id); 62 | 63 | io.to(user.room).emit("message", formatMessage(user.username, msg)); 64 | }); 65 | 66 | // Runs when client disconnects 67 | socket.on("disconnect", () => { 68 | const user = userLeave(socket.id); 69 | 70 | if (user) { 71 | io.to(user.room).emit( 72 | "message", 73 | formatMessage(botName, `${user.username} has left the chat`) 74 | ); 75 | 76 | // Send users and room info 77 | io.to(user.room).emit("roomUsers", { 78 | room: user.room, 79 | users: getRoomUsers(user.room), 80 | }); 81 | } 82 | }); 83 | }); 84 | 85 | const PORT = process.env.PORT || 3000; 86 | 87 | server.listen(PORT, () => console.log(`Server running on port ${PORT}`)); 88 | -------------------------------------------------------------------------------- /_html_css/css/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto&display=swap'); 2 | 3 | :root { 4 | --dark-color-a: #667aff; 5 | --dark-color-b: #7386ff; 6 | --light-color: #e6e9ff; 7 | --success-color: #5cb85c; 8 | --error-color: #d9534f; 9 | } 10 | 11 | * { 12 | box-sizing: border-box; 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | body { 18 | font-family: 'Roboto', sans-serif; 19 | font-size: 16px; 20 | background: var(--light-color); 21 | margin: 20px; 22 | } 23 | 24 | ul { 25 | list-style: none; 26 | } 27 | 28 | a { 29 | text-decoration: none; 30 | } 31 | 32 | .btn { 33 | cursor: pointer; 34 | padding: 5px 15px; 35 | background: var(--light-color); 36 | color: var(--dark-color-a); 37 | border: 0; 38 | font-size: 17px; 39 | } 40 | 41 | /* Chat Page */ 42 | 43 | .chat-container { 44 | max-width: 1100px; 45 | background: #fff; 46 | margin: 30px auto; 47 | overflow: hidden; 48 | } 49 | 50 | .chat-header { 51 | background: var(--dark-color-a); 52 | color: #fff; 53 | border-top-left-radius: 5px; 54 | border-top-right-radius: 5px; 55 | padding: 15px; 56 | display: flex; 57 | align-items: center; 58 | justify-content: space-between; 59 | } 60 | 61 | .chat-main { 62 | display: grid; 63 | grid-template-columns: 1fr 3fr; 64 | } 65 | 66 | .chat-sidebar { 67 | background: var(--dark-color-b); 68 | color: #fff; 69 | padding: 20px 20px 60px; 70 | overflow-y: scroll; 71 | } 72 | 73 | .chat-sidebar h2 { 74 | font-size: 20px; 75 | background: rgba(0, 0, 0, 0.1); 76 | padding: 10px; 77 | margin-bottom: 20px; 78 | } 79 | 80 | .chat-sidebar h3 { 81 | margin-bottom: 15px; 82 | } 83 | 84 | .chat-sidebar ul li { 85 | padding: 10px 0; 86 | } 87 | 88 | .chat-messages { 89 | padding: 30px; 90 | max-height: 500px; 91 | overflow-y: scroll; 92 | } 93 | 94 | .chat-messages .message { 95 | padding: 10px; 96 | margin-bottom: 15px; 97 | background-color: var(--light-color); 98 | border-radius: 5px; 99 | } 100 | 101 | .chat-messages .message .meta { 102 | font-size: 15px; 103 | font-weight: bold; 104 | color: var(--dark-color-b); 105 | opacity: 0.7; 106 | margin-bottom: 7px; 107 | } 108 | 109 | .chat-messages .message .meta span { 110 | color: #777; 111 | } 112 | 113 | .chat-form-container { 114 | padding: 20px 30px; 115 | background-color: var(--dark-color-a); 116 | } 117 | 118 | .chat-form-container form { 119 | display: flex; 120 | } 121 | 122 | .chat-form-container input[type='text'] { 123 | font-size: 16px; 124 | padding: 5px; 125 | height: 40px; 126 | flex: 1; 127 | } 128 | 129 | /* Join Page */ 130 | .join-container { 131 | max-width: 500px; 132 | margin: 80px auto; 133 | color: #fff; 134 | } 135 | 136 | .join-header { 137 | text-align: center; 138 | padding: 20px; 139 | background: var(--dark-color-a); 140 | border-top-left-radius: 5px; 141 | border-top-right-radius: 5px; 142 | } 143 | 144 | .join-main { 145 | padding: 30px 40px; 146 | background: var(--dark-color-b); 147 | } 148 | 149 | .join-main p { 150 | margin-bottom: 20px; 151 | } 152 | 153 | .join-main .form-control { 154 | margin-bottom: 20px; 155 | } 156 | 157 | .join-main label { 158 | display: block; 159 | margin-bottom: 5px; 160 | } 161 | 162 | .join-main input[type='text'] { 163 | font-size: 16px; 164 | padding: 5px; 165 | height: 40px; 166 | width: 100%; 167 | } 168 | 169 | .join-main select { 170 | font-size: 16px; 171 | padding: 5px; 172 | height: 40px; 173 | width: 100%; 174 | } 175 | 176 | .join-main .btn { 177 | margin-top: 20px; 178 | width: 100%; 179 | } 180 | 181 | @media (max-width: 700px) { 182 | .chat-main { 183 | display: block; 184 | } 185 | 186 | .chat-sidebar { 187 | display: none; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto&display=swap'); 2 | 3 | :root { 4 | --dark-color-a: #667aff; 5 | --dark-color-b: #7386ff; 6 | --light-color: #e6e9ff; 7 | --success-color: #5cb85c; 8 | --error-color: #d9534f; 9 | } 10 | 11 | * { 12 | box-sizing: border-box; 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | body { 18 | font-family: 'Roboto', sans-serif; 19 | font-size: 16px; 20 | background: var(--light-color); 21 | margin: 20px; 22 | } 23 | 24 | ul { 25 | list-style: none; 26 | } 27 | 28 | a { 29 | text-decoration: none; 30 | } 31 | 32 | .btn { 33 | cursor: pointer; 34 | padding: 5px 15px; 35 | background: var(--light-color); 36 | color: var(--dark-color-a); 37 | border: 0; 38 | font-size: 17px; 39 | } 40 | 41 | /* Chat Page */ 42 | 43 | .chat-container { 44 | max-width: 1100px; 45 | background: #fff; 46 | margin: 30px auto; 47 | overflow: hidden; 48 | } 49 | 50 | .chat-header { 51 | background: var(--dark-color-a); 52 | color: #fff; 53 | border-top-left-radius: 5px; 54 | border-top-right-radius: 5px; 55 | padding: 15px; 56 | display: flex; 57 | align-items: center; 58 | justify-content: space-between; 59 | } 60 | 61 | .chat-main { 62 | display: grid; 63 | grid-template-columns: 1fr 3fr; 64 | } 65 | 66 | .chat-sidebar { 67 | background: var(--dark-color-b); 68 | color: #fff; 69 | padding: 20px 20px 60px; 70 | overflow-y: scroll; 71 | } 72 | 73 | .chat-sidebar h2 { 74 | font-size: 20px; 75 | background: rgba(0, 0, 0, 0.1); 76 | padding: 10px; 77 | margin-bottom: 20px; 78 | } 79 | 80 | .chat-sidebar h3 { 81 | margin-bottom: 15px; 82 | } 83 | 84 | .chat-sidebar ul li { 85 | padding: 10px 0; 86 | } 87 | 88 | .chat-messages { 89 | padding: 30px; 90 | max-height: 500px; 91 | overflow-y: scroll; 92 | } 93 | 94 | .chat-messages .message { 95 | padding: 10px; 96 | margin-bottom: 15px; 97 | background-color: var(--light-color); 98 | border-radius: 5px; 99 | overflow-wrap: break-word; 100 | } 101 | 102 | .chat-messages .message .meta { 103 | font-size: 15px; 104 | font-weight: bold; 105 | color: var(--dark-color-b); 106 | opacity: 0.7; 107 | margin-bottom: 7px; 108 | } 109 | 110 | .chat-messages .message .meta span { 111 | color: #777; 112 | } 113 | 114 | .chat-form-container { 115 | padding: 20px 30px; 116 | background-color: var(--dark-color-a); 117 | } 118 | 119 | .chat-form-container form { 120 | display: flex; 121 | } 122 | 123 | .chat-form-container input[type='text'] { 124 | font-size: 16px; 125 | padding: 5px; 126 | height: 40px; 127 | flex: 1; 128 | } 129 | 130 | /* Join Page */ 131 | .join-container { 132 | max-width: 500px; 133 | margin: 80px auto; 134 | color: #fff; 135 | } 136 | 137 | .join-header { 138 | text-align: center; 139 | padding: 20px; 140 | background: var(--dark-color-a); 141 | border-top-left-radius: 5px; 142 | border-top-right-radius: 5px; 143 | } 144 | 145 | .join-main { 146 | padding: 30px 40px; 147 | background: var(--dark-color-b); 148 | } 149 | 150 | .join-main p { 151 | margin-bottom: 20px; 152 | } 153 | 154 | .join-main .form-control { 155 | margin-bottom: 20px; 156 | } 157 | 158 | .join-main label { 159 | display: block; 160 | margin-bottom: 5px; 161 | } 162 | 163 | .join-main input[type='text'] { 164 | font-size: 16px; 165 | padding: 5px; 166 | height: 40px; 167 | width: 100%; 168 | } 169 | 170 | .join-main select { 171 | font-size: 16px; 172 | padding: 5px; 173 | height: 40px; 174 | width: 100%; 175 | } 176 | 177 | .join-main .btn { 178 | margin-top: 20px; 179 | width: 100%; 180 | } 181 | 182 | @media (max-width: 700px) { 183 | .chat-main { 184 | display: block; 185 | } 186 | 187 | .chat-sidebar { 188 | display: none; 189 | } 190 | } 191 | --------------------------------------------------------------------------------