├── .gitignore ├── src ├── config.js ├── public │ ├── img │ │ └── typing-indicator2.gif │ ├── css │ │ └── main.css │ ├── js │ │ └── main.js │ └── index.html ├── index.js ├── app.js └── sockets.js ├── docs └── Screenshot-v0.0.1.png ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export const PORT = process.env.PORT || 3000; 2 | -------------------------------------------------------------------------------- /docs/Screenshot-v0.0.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fazt/VueChat/HEAD/docs/Screenshot-v0.0.1.png -------------------------------------------------------------------------------- /src/public/img/typing-indicator2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fazt/VueChat/HEAD/src/public/img/typing-indicator2.gif -------------------------------------------------------------------------------- /src/public/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 20px; 3 | background-color: #343434; 4 | } 5 | 6 | .chat-list { 7 | overflow-y: scroll; 8 | height: 700px; 9 | padding: 0; 10 | } 11 | 12 | .chat-list::-webkit-scrollbar { 13 | display: none; 14 | } 15 | 16 | .message { 17 | list-style: none; 18 | } 19 | 20 | .message img { 21 | padding: 10px; 22 | } 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import app from "./app.js"; 3 | import { Server as SocketServer } from "socket.io"; 4 | import sockets from "./sockets.js"; 5 | import { PORT } from "./config.js"; 6 | 7 | const server = http.createServer(app); 8 | const io = new SocketServer(server); 9 | 10 | app.set("io", io); 11 | 12 | // sockets 13 | sockets(io); 14 | 15 | server.listen(PORT); 16 | console.log(`Server listening on port ${PORT}`); 17 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import morgan from "morgan"; 3 | import { dirname, join } from "path"; 4 | import { fileURLToPath } from "url"; 5 | 6 | const app = express(); 7 | const __dirname = dirname(fileURLToPath(import.meta.url)); 8 | 9 | // middlewares 10 | app.use(morgan("dev")); 11 | 12 | app.get("/onlineusers", (req, res) => { 13 | console.log([...app.get("io").sockets.adapter.rooms.keys()]); 14 | res.send([...(app.get("io").sockets.adapter.rooms.keys())]); 15 | }); 16 | 17 | // static files 18 | app.use(express.static(join(__dirname, "public"))); 19 | 20 | export default app; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuechat", 3 | "version": "0.0.2", 4 | "description": "A basic Chat with Vue.js and Node.js, with express and socket.io", 5 | "main": "src/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "nodemon ./src/index.js --ignore src/public", 9 | "start": "node ./src/index.js" 10 | }, 11 | "keywords": [ 12 | "chat", 13 | "Vue.js", 14 | "vue", 15 | "express", 16 | "node.js", 17 | "node", 18 | "socket.io", 19 | "sockets", 20 | "es6", 21 | "bootstrap" 22 | ], 23 | "author": "Fazt", 24 | "license": "ISC", 25 | "dependencies": { 26 | "express": "^4.18.1", 27 | "morgan": "^1.10.0", 28 | "socket.io": "^4.5.1" 29 | }, 30 | "devDependencies": { 31 | "nodemon": "^2.0.16" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ChatVue 2 | 3 | this is a web application using Websocket with VUe 4 | 5 | ![](docs/Screenshot-v0.0.1.png) 6 | 7 | ## Tutorial 8 | 9 | -[Youtube Channel](https://www.youtube.com/channel/UCX9NJ471o7Wie1DQe94RVIg) 10 | 11 | ## Backend Tools 12 | 13 | - Node.js 14 | - Express 15 | - Socket.io 16 | - npm 17 | - Babel 18 | 19 | ## FrontEnd Tools 20 | 21 | - Vue.js 22 | - Font Awesome 23 | - Bootstrap5 24 | - [Bootswatch](https://bootswatch.com/) 25 | - [uiGradients](https://uigradients.com) 26 | - Placeholder Images 27 | - [LoremPixel](http://lorempixel.com/) 28 | - [Placeholder](https://placeholder.com/) 29 | - [Moment.js](https://momentjs.com/) 30 | 31 | ## What is Next? 32 | 33 | - use a real avatar image 34 | - save messages on a database 35 | - multiple chat rooms 36 | - private messages 37 | - emojis 38 | - add SQL ORM (Sequelize maybe) 39 | - add docker -------------------------------------------------------------------------------- /src/sockets.js: -------------------------------------------------------------------------------- 1 | export default (io) => { 2 | io.on("connection", (socket) => { 3 | // Log a new user connected 4 | console.log(`A new Used Connected ${socket.id}`); 5 | 6 | // tell all clients that someone connected 7 | io.emit("user joined", socket.id); 8 | 9 | //the client sends 'chat:message event' 10 | socket.on("chat:message", function (message) { 11 | // Emit this Event to all clients connected 12 | io.emit("chat:message", message); 13 | }); 14 | 15 | //client sends "user typing" event to server 16 | socket.on("user typing", function (username) { 17 | io.emit("user typing", username); 18 | }); 19 | 20 | //client sends "stopped typing" event to server 21 | socket.on("stopped typing", function (username) { 22 | io.emit("stopped typing", username); 23 | }); 24 | 25 | // when a new user is disconnected 26 | socket.on("disconnect", function () { 27 | console.log(`User left ${socket.id}`); 28 | 29 | //tell all clients that someone disconnected 30 | socket.broadcast.emit("user left", socket.id); 31 | }); 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /src/public/js/main.js: -------------------------------------------------------------------------------- 1 | // Socket io connection 2 | const socket = io(); 3 | 4 | // Vuejs Component 5 | Vue.createApp({ 6 | data() { 7 | return { 8 | connectedUsers: [], 9 | messages: [], 10 | message: { 11 | type: "", 12 | action: "", 13 | user: "", 14 | text: "", 15 | timestamp: "", 16 | }, 17 | areTyping: [], 18 | viewChatBox: true, 19 | }; 20 | }, 21 | mounted() { 22 | // focus the input message field 23 | this.$refs.inputMessage.focus(); 24 | 25 | // when 'user joined', update connectedUsers arrays 26 | socket.on("user joined", async (socketId) => { 27 | const response = await axios.get("/onlineusers"); 28 | 29 | // all connected users except the current user 30 | const sockets = response.data.filter((socket) => socket !== socketId); 31 | 32 | // update connectedUsers array 33 | this.connectedUsers = sockets; 34 | 35 | const infoMsg = { 36 | type: "info", 37 | msg: "User" + socketId + " has joined", 38 | }; 39 | 40 | this.messages.push(infoMsg); 41 | }); 42 | 43 | // chat event messages 44 | socket.on("chat:message", (message) => this.messages.push(message)); 45 | 46 | // chat event 'user typing' 47 | socket.on("user typing", (username) => this.areTyping.push(username)); 48 | 49 | // server emits 'stopped typing' 50 | socket.on("stopped typing", (username) => { 51 | var index = this.areTyping.indexOf(username); 52 | if (index >= 0) { 53 | this.areTyping.splice(index, 1); 54 | } 55 | }); 56 | 57 | //if 'user left' remove from the connectedUsers array 58 | socket.on("user left", (socketId) => { 59 | var index = this.connectedUsers.indexOf(socketId); 60 | if (index >= 0) { 61 | this.connectedUsers.splice(index, 1); 62 | } 63 | 64 | var infoMsg = { 65 | type: "info", 66 | msg: "User" + socketId + " has joined", 67 | }; 68 | this.messages.push(infoMsg); 69 | }); 70 | }, 71 | methods: { 72 | send() { 73 | this.message.type = "chat"; 74 | this.message.user = this.message.user || socket.id; 75 | this.message.timestamp = moment().calendar(); 76 | socket.emit("chat:message", this.message); 77 | 78 | // cleanning data 79 | this.message.type = ""; 80 | this.message.timestamp = ""; 81 | this.message.text = ""; 82 | }, 83 | userIsTyping(username) { 84 | if (this.areTyping.indexOf(username) >= 0) { 85 | return true; 86 | } 87 | return false; 88 | }, 89 | usersAreTyping() { 90 | if (this.areTyping.indexOf(socket.id) <= -1) { 91 | this.areTyping.push(socket.id); 92 | socket.emit("user typing", socket.id); 93 | } 94 | }, 95 | stoppedTyping(keyCode) { 96 | if (keyCode == "13" || (keyCode == "8" && this.message.text == "")) { 97 | var index = this.areTyping.indexOf(socket.id); 98 | if (index >= 0) { 99 | this.areTyping.splice(index, 1); 100 | socket.emit("stopped typing", socket.id); 101 | } 102 | } 103 | }, 104 | }, 105 | }).mount("#app"); 106 | -------------------------------------------------------------------------------- /src/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ChatVue 6 | 7 | 13 | 14 | 15 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 | 32 |
33 |
34 |
35 | 36 | Online ({{connectedUsers.length}}) 37 |
38 |
39 |
    40 |
  • 41 | {{user}}   42 | 47 | 52 | 53 |
  • 54 |
55 |
56 |
57 | 58 |
59 |
60 |

ChatVue

61 |
62 |
63 |

Choose a Username:

64 | 70 |
71 | 80 |
81 |
82 | 83 |
84 |
85 |
88 | 89 | 90 | Let's Chat 91 | 92 | 93 | 94 | 95 |
96 |
97 |
    98 |
  • 102 |
    103 | {{ message.msg }} 104 |
    105 | 106 |
    111 | User Image Profile 117 |
    118 |
    119 | {{message.user}} 120 | 121 |   {{ message.timestamp }} 122 | 123 |
    124 |

    {{ message.text }}

    125 |
    126 |
    127 |
  • 128 |
129 |
130 | 155 |
156 |
157 |
158 |
159 | 160 | 161 | 165 | 166 | 167 | 168 | 169 | 170 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | --------------------------------------------------------------------------------