├── _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 | [](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 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
13 |
14 | ChatCord App
15 |
16 |
17 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/public/chat.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
13 | ChatCord App
14 |
15 |
16 |
17 |
21 |
22 |
28 |
29 |
30 |
31 |
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 |
16 |
17 |
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 |
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 |
--------------------------------------------------------------------------------