├── README.md ├── public ├── img │ └── icons8-chat-64.png ├── js │ └── script.js ├── index.html ├── chat.html └── css │ └── style.css ├── utils ├── messages.js └── users.js ├── package.json ├── app.js └── .gitignore /README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/img/icons8-chat-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frozen-dev71/Real-Time-Chat-Application/main/public/img/icons8-chat-64.png -------------------------------------------------------------------------------- /utils/messages.js: -------------------------------------------------------------------------------- 1 | // dependencies 2 | const moment = require('moment-timezone'); 3 | 4 | function formatMessage(username, text) { 5 | return { 6 | username, 7 | text, 8 | // time: moment().format('h:mm a'), 9 | time: moment().tz('Asia/Dhaka').format('h:mm a'), 10 | }; 11 | } 12 | 13 | module.exports = formatMessage; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ChatApplication", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node app.js" 9 | }, 10 | "keywords": [], 11 | "author": "Mehedi Islam Ripon", 12 | "license": "ISC", 13 | "dependencies": { 14 | "express": "^4.17.1", 15 | "moment-timezone": "^0.5.33", 16 | "socket.io": "^4.1.1" 17 | }, 18 | "engines": { 19 | "node": ">=10.0.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/js/script.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 | // het message text 36 | const msg = e.target.elements.msg.value; 37 | 38 | // emit message to server 39 | socket.emit('chatMessage', msg); 40 | 41 | // clear input 42 | e.target.elements.msg.value = ''; 43 | e.target.elements.msg.focus(); 44 | }); 45 | 46 | // output msg to DOM 47 | function outputMessage(message) { 48 | const div = document.createElement('div'); 49 | div.classList.add('message'); 50 | div.innerHTML = `

${message.username} ${message.time}

51 |

52 | ${message.text} 53 |

`; 54 | 55 | document.querySelector('.chat-messages').appendChild(div); 56 | } 57 | 58 | // add room name to DOM 59 | function outputRoomName(room) { 60 | roomName.innerHTML = room; 61 | } 62 | 63 | // add users to DOM 64 | function outputUsers(users) { 65 | userList.innerHTML = ` 66 | ${users.map((user) => `
  • ${user.username}
  • `).join('')} 67 | `; 68 | } 69 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // dependencies 2 | const express = require('express'); 3 | const http = require('http'); 4 | const socketIo = require('socket.io'); 5 | const path = require('path'); 6 | const formatMessage = require('./utils/messages'); 7 | const { 8 | userJoin, 9 | getCurrentUser, 10 | userLeave, 11 | getRoomUsers, 12 | } = require('./utils/users'); 13 | 14 | const app = express(); 15 | const server = http.createServer(app); 16 | const io = socketIo(server); 17 | 18 | // set static file 19 | app.use(express.static(path.join(__dirname, 'public'))); 20 | 21 | const botName = 'XeroxChat Bot'; 22 | 23 | // run when client connects 24 | io.on('connection', (socket) => { 25 | socket.on('joinRoom', ({ username, room }) => { 26 | const user = userJoin(socket.id, username, room); 27 | 28 | socket.join(user.room); 29 | 30 | // welcome current user 31 | socket.emit('message', formatMessage(botName, 'Welcome to XeroxChat!')); 32 | 33 | // broadcast when a user connects 34 | socket.broadcast 35 | .to(user.room) 36 | .emit( 37 | 'message', 38 | formatMessage(botName, `${user.username} has joined the chat!`) 39 | ); 40 | 41 | // send users and room info 42 | io.to(user.room).emit('roomUsers', { 43 | room: user.room, 44 | users: getRoomUsers(user.room), 45 | }); 46 | }); 47 | 48 | // listen for chatMessage 49 | socket.on('chatMessage', (msg) => { 50 | const user = getCurrentUser(socket.id); 51 | 52 | io.to(user.room).emit('message', formatMessage(user.username, msg)); 53 | }); 54 | 55 | // runs when clients disconnects 56 | socket.on('disconnect', () => { 57 | const user = userLeave(socket.id); 58 | 59 | if (user) { 60 | io.to(user.room).emit( 61 | 'message', 62 | formatMessage(botName, `${user.username} has left the chat!`) 63 | ); 64 | 65 | // send users and room info 66 | io.to(user.room).emit('roomUsers', { 67 | room: user.room, 68 | users: getRoomUsers(user.room), 69 | }); 70 | } 71 | }); 72 | }); 73 | 74 | const PORT = process.env.PORT || 3000; 75 | server.listen(PORT, () => { 76 | console.log(`🎯 Server is running on PORT: ${PORT}`); 77 | }); 78 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 18 | 19 | Chat Application by Mehedi 20 | 21 | 22 |
    23 |
    24 |

    25 | XeroxChat 26 |

    27 |
    28 |
    29 |
    30 |
    31 | 32 | 39 |
    40 |
    41 | 42 | 49 |
    50 | 51 |
    52 |
    53 | 58 |
    59 | 60 | 61 | -------------------------------------------------------------------------------- /public/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 17 | 18 | Chat Application by Mehedi 19 | 20 | 21 |
    22 |
    23 |

    24 | XeroxChat 25 |

    26 | Leave Room 27 |
    28 |
    29 |
    30 |

    Room Name:

    31 |

    32 |

    Users

    33 |
      34 |
      35 |
      36 |
      37 |
      38 |
      39 | 46 | 49 |
      50 |
      51 |
      52 | 53 | 58 | 59 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | .env*.local 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | .parcel-cache 83 | 84 | # Next.js build output 85 | .next 86 | 87 | # Nuxt.js build / generate output 88 | .nuxt 89 | dist 90 | 91 | # Storybook build outputs 92 | .out 93 | .storybook-out 94 | storybook-static 95 | 96 | # rollup.js default build output 97 | dist/ 98 | 99 | # Gatsby files 100 | .cache/ 101 | # Comment in the public line in if your project uses Gatsby and not Next.js 102 | # https://nextjs.org/blog/next-9-1#public-directory-support 103 | # public 104 | 105 | # vuepress build output 106 | .vuepress/dist 107 | 108 | # Serverless directories 109 | .serverless/ 110 | 111 | # FuseBox cache 112 | .fusebox/ 113 | 114 | # DynamoDB Local files 115 | .dynamodb/ 116 | 117 | # TernJS port file 118 | .tern-port 119 | 120 | # Stores VSCode versions used for testing VSCode extensions 121 | .vscode-test 122 | 123 | # Temporary folders 124 | tmp/ 125 | temp/ 126 | 127 | ### VisualStudioCode ### 128 | .vscode/* 129 | !.vscode/settings.json 130 | !.vscode/tasks.json 131 | !.vscode/launch.json 132 | !.vscode/extensions.json 133 | *.code-workspace 134 | 135 | ### VisualStudioCode Patch ### 136 | # Ignore all local history of files 137 | .history 138 | .ionide 139 | 140 | ### Windows ### 141 | # Windows thumbnail cache files 142 | Thumbs.db 143 | Thumbs.db:encryptable 144 | ehthumbs.db 145 | ehthumbs_vista.db 146 | 147 | # Dump file 148 | *.stackdump 149 | 150 | # Folder config file 151 | [Dd]esktop.ini 152 | 153 | # Recycle Bin used on file shares 154 | $RECYCLE.BIN/ 155 | 156 | # Windows Installer files 157 | *.cab 158 | *.msi 159 | *.msix 160 | *.msm 161 | *.msp 162 | 163 | # Windows shortcuts 164 | *.lnk -------------------------------------------------------------------------------- /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: #1b1b32; 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 | border: none; 36 | border-radius: 5px; 37 | background: #b1d8eb; 38 | color: #3f5163; 39 | border: none; 40 | font-size: 17px; 41 | } 42 | 43 | /* Chat Page */ 44 | .chat-header .btn { 45 | cursor: pointer; 46 | padding: 5px 15px; 47 | border: none; 48 | border-radius: 25px; 49 | background: #b1d8eb; 50 | color: #3f5163; 51 | border: none; 52 | font-size: 17px; 53 | } 54 | 55 | .chat-container { 56 | max-width: 1100px; 57 | margin: 30px auto; 58 | overflow: hidden; 59 | } 60 | 61 | .chat-header { 62 | background: #3b3b4f; 63 | color: #fff; 64 | /* border-top-left-radius: 5px; 65 | border-top-right-radius: 5px; */ 66 | padding: 15px; 67 | border-radius: 5px 5px 0px 0px; 68 | display: flex; 69 | align-items: center; 70 | justify-content: space-between; 71 | } 72 | 73 | .chat-main { 74 | display: grid; 75 | grid-template-columns: 1fr 3fr; 76 | } 77 | 78 | .chat-sidebar { 79 | background: #323244; 80 | color: #fff; 81 | padding: 20px 20px 60px; 82 | overflow-y: scroll; 83 | } 84 | 85 | .chat-sidebar h2 { 86 | font-size: 20px; 87 | background: rgba(0, 0, 0, 0.1); 88 | padding: 10px; 89 | margin-bottom: 20px; 90 | } 91 | 92 | .chat-sidebar h3 { 93 | margin-bottom: 15px; 94 | } 95 | 96 | .chat-sidebar ul li { 97 | padding: 10px 0; 98 | } 99 | 100 | .chat-messages { 101 | padding: 30px; 102 | max-height: 500px; 103 | overflow-y: scroll; 104 | background: #34495e; 105 | min-height: 68vh; 106 | } 107 | 108 | .chat-messages .message { 109 | padding: 10px; 110 | margin-bottom: 15px; 111 | background-color: #aac0c1; 112 | border-radius: 5px; 113 | overflow-wrap: break-word; 114 | } 115 | 116 | .chat-messages .message .meta { 117 | font-size: 15px; 118 | font-weight: bold; 119 | color: #34495e; 120 | opacity: 0.7; 121 | margin-bottom: 7px; 122 | } 123 | 124 | .chat-messages .message .meta span { 125 | color: #777; 126 | } 127 | 128 | .chat-form-container { 129 | padding: 20px 30px; 130 | background-color: #3b3b4f; 131 | border-radius: 0 0 5px 5px; 132 | } 133 | 134 | textarea:focus, 135 | input:focus { 136 | outline: none; 137 | } 138 | 139 | .chat-form-container input[type='text'] { 140 | font-size: 16px; 141 | padding: 0px 20px; 142 | border: none; 143 | height: 40px; 144 | width: 86%; 145 | background: #d9edf7; 146 | border-radius: 18px; 147 | } 148 | 149 | /* Join Page */ 150 | .join-container { 151 | max-width: 500px; 152 | margin: 80px auto; 153 | color: #fff; 154 | } 155 | 156 | .join-header { 157 | text-align: center; 158 | padding: 20px; 159 | background: #3b3b4f; 160 | border-top-left-radius: 5px; 161 | border-top-right-radius: 5px; 162 | } 163 | 164 | .join-main { 165 | padding: 30px 40px; 166 | background: #323244; 167 | } 168 | 169 | .join-main p { 170 | margin-bottom: 20px; 171 | } 172 | 173 | .join-main .form-control { 174 | margin-bottom: 20px; 175 | } 176 | 177 | .join-main label { 178 | display: block; 179 | margin-bottom: 8px; 180 | } 181 | 182 | .join-main input[type='text'] { 183 | font-size: 16px; 184 | padding: 20px; 185 | height: 40px; 186 | border-radius: 5px; 187 | border: none; 188 | background: #d9edf7; 189 | width: 100%; 190 | } 191 | 192 | .join-main select { 193 | font-size: 16px; 194 | padding: 10px; 195 | height: 40px; 196 | width: 100%; 197 | border: none; 198 | border-radius: 5px; 199 | color: #757575; 200 | background: #d9edf7; 201 | } 202 | 203 | .join-main .btn { 204 | margin-top: 20px; 205 | width: 30%; 206 | padding: 10px; 207 | border: none; 208 | border-radius: 5px; 209 | color: #34495e; 210 | background: #b1d8eb; 211 | } 212 | 213 | @media (max-width: 700px) { 214 | .chat-main { 215 | display: block; 216 | } 217 | 218 | .chat-sidebar { 219 | display: none; 220 | } 221 | } 222 | 223 | @media only screen and (max-width: 426px) { 224 | .join-main .btn { 225 | width: 46%; 226 | padding: 8px; 227 | } 228 | } 229 | 230 | @media only screen and (max-width: 1138px) { 231 | .chat-form-container input[type='text'] { 232 | width: 60%; 233 | } 234 | } 235 | 236 | @media only screen and (max-width: 464px) { 237 | .chat-form-container input[type='text'] { 238 | width: 100%; 239 | } 240 | } 241 | 242 | .send-button { 243 | cursor: pointer; 244 | padding: 10px 30px; 245 | border: none; 246 | border-radius: 5px; 247 | background: #b1d8eb; 248 | color: #3f5163; 249 | border: none; 250 | border-radius: 25px; 251 | font-size: 17px; 252 | margin-left: 20px; 253 | } 254 | 255 | @media only screen and (max-width: 464px) { 256 | .send-button { 257 | margin-left: 0px; 258 | margin-top: 10px; 259 | } 260 | .chat-messages { 261 | min-height: 65vh; 262 | } 263 | } 264 | --------------------------------------------------------------------------------